Update server to work with the latest version :3
This commit is contained in:
parent
d73c7e44d5
commit
a7b9b5b87f
6 changed files with 186 additions and 46 deletions
|
@ -1,4 +1,7 @@
|
|||
FROM python:3.11
|
||||
ADD https://github.com/krallin/tini/releases/download/v0.19.0/tini /tini
|
||||
RUN chmod +x /tini
|
||||
ENTRYPOINT ["/tini", "--"]
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
|
@ -6,4 +9,4 @@ COPY . .
|
|||
|
||||
RUN --mount=type=cache,target=/tmp/pip pip install . --cache-dir /tmp/pip
|
||||
|
||||
CMD python3 -m customiwmserver
|
||||
CMD python3 -m uvicorn customiwmserver.main:app --host 0.0.0.0 --port 8001 --reload
|
||||
|
|
|
@ -2,10 +2,19 @@ from pydantic import BaseModel
|
|||
from typing import List, Optional, Union
|
||||
from datetime import datetime
|
||||
|
||||
class MapPlay(BaseModel):
|
||||
"""Used to store map playthroughs for players"""
|
||||
Played: bool = True # Obviously
|
||||
Clear: bool = False
|
||||
FullClear: bool = False
|
||||
|
||||
class User(BaseModel):
|
||||
"""Pydantic model for user data.
|
||||
Created from the user response (`/api/v1/user/x`),
|
||||
some values might be unused."""
|
||||
some values might be unused.
|
||||
Color values are decimal versions of joined hexadecimal codes
|
||||
(for example (0xFF, 0x00, 0x00) is joined to 0xFF0000 and converted to 16711680)
|
||||
"""
|
||||
|
||||
Admin: bool = True
|
||||
Banned: bool = False
|
||||
|
@ -55,7 +64,12 @@ class User(BaseModel):
|
|||
|
||||
# Ratings have to be stored in the following format: {mapID: rating},
|
||||
# where rating is either 1 or 5, or None.
|
||||
Ratings: dict = {}
|
||||
Ratings: dict[str, Optional[int]] = {}
|
||||
|
||||
# Plays are stored in the following format:
|
||||
# {mapID: {}}
|
||||
# (str type is used instead of an int because MongoDB)
|
||||
Plays: dict[str, MapPlay] = {}
|
||||
|
||||
|
||||
class Notification(BaseModel):
|
||||
|
@ -160,7 +174,8 @@ class Map(BaseModel):
|
|||
MapCode: str = "AAABBAAA"
|
||||
Listed: bool = True
|
||||
HiddenInChallenges: bool = False
|
||||
DragonCoins: bool = False
|
||||
ReplayVisibility: int = 0
|
||||
DragonCoins: bool = False # Gems
|
||||
|
||||
# Might be duplicates from the User data
|
||||
|
||||
|
@ -216,6 +231,10 @@ class Map(BaseModel):
|
|||
BestTimePlaytime: int = 0
|
||||
MyBestPlaytime: int = 0
|
||||
|
||||
BestFullTimeUserID: int = 0
|
||||
BestFullTimeUsername: str = ""
|
||||
BestFullTimePlaytime: int = 0
|
||||
|
||||
FirstClearUserID: Union[int, None] = 0
|
||||
|
||||
# Propably not needed, because you can simply read the
|
||||
|
@ -240,6 +259,7 @@ class MapLeaderboard(BaseModel):
|
|||
BestPlaytime: int
|
||||
BestPlaytimeTime: str = datetime.strftime(datetime.now(), "%Y-%m-%dT%H:%M:%SZ")
|
||||
BestReplay: str
|
||||
FullClear: bool = False
|
||||
CreatorName: str
|
||||
UserID: int
|
||||
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
from datetime import datetime
|
||||
from typing import Literal
|
||||
from pymongo import MongoClient
|
||||
|
||||
|
||||
|
@ -28,25 +29,26 @@ def LogAdminAction(
|
|||
)
|
||||
|
||||
|
||||
def auth_check(Authorization):
|
||||
def auth_check(Authorization) -> (tuple[Literal[False], Literal["noauth"]] | tuple[Literal[True], dict]):
|
||||
"""Checks credentials.
|
||||
Returns a tuple with result (for example False, "nouser").
|
||||
|
||||
Results:
|
||||
- nouser = user not found
|
||||
- wrongpass = wrong password
|
||||
- [dictionary] = query
|
||||
- False if wrong username or password
|
||||
- True, [dict] if correct
|
||||
"""
|
||||
# FIXME: This function currently DOES NOT perform any authentication.
|
||||
# This means that ANYONE knowing the username could perform actions as the user.
|
||||
if Authorization is None:
|
||||
return False, "noauth"
|
||||
username, password = Authorization.split(":")
|
||||
|
||||
query = user_collection.find_one({"Username": username})
|
||||
if not query:
|
||||
return False, "nouser"
|
||||
return False, "noauth"
|
||||
|
||||
if query["Password"] != password:
|
||||
return False, "wrongpass"
|
||||
# if query["Password"] != password:
|
||||
# return False, "wrongpass"
|
||||
|
||||
return True, query
|
||||
|
||||
|
|
|
@ -1,5 +1,4 @@
|
|||
# from . import *
|
||||
|
||||
import pkgutil
|
||||
|
||||
__path__ = pkgutil.extend_path(__path__, __name__)
|
||||
|
|
|
@ -38,10 +38,7 @@ async def login(username: str = Form(), password: str = Form(), version: str = F
|
|||
hook.execute_hooks("player_login", username=username)
|
||||
auth = db.auth_check(username + ":" + password)
|
||||
if not auth[0]:
|
||||
if auth[1] == "nouser":
|
||||
raise HTTPException(404, detail="User not found.")
|
||||
elif auth[1] == "wrongpass":
|
||||
raise HTTPException(403, detail="Password is incorrect.")
|
||||
raise HTTPException(403, detail="Wrong username or password.")
|
||||
else:
|
||||
return {"token": username + ":" + password, "userId": auth[1]["ID"]}
|
||||
|
||||
|
@ -72,9 +69,43 @@ async def create_user(
|
|||
@app.get("/api/v1/notifunread")
|
||||
async def notifunread():
|
||||
"""Returns the number of unread notifications."""
|
||||
# FIXME Add notifications
|
||||
# FIXME: Add notifications
|
||||
return 0
|
||||
|
||||
@app.post("/api/v1/user/{userID}")
|
||||
async def update_user_profile(
|
||||
userID: int,
|
||||
Authorization: Union[str, None] = Header(default=None),
|
||||
ID: int = Body(),
|
||||
Country: int = Body(),
|
||||
PantsColor: int = Body(),
|
||||
HatSpr: int = Body(),
|
||||
HairColor: int = Body(),
|
||||
CapeColor: int = Body(),
|
||||
ShoesColor: int = Body(),
|
||||
ShirtColor: int = Body(),
|
||||
SkinColor: int = Body(),
|
||||
):
|
||||
authcheck = db.auth_check(Authorization)
|
||||
if not authcheck[0]:
|
||||
raise HTTPException(403, detail="Wrong username or password")
|
||||
elif authcheck[0]:
|
||||
userData = types.User(**authcheck[1])
|
||||
userData.Country = Country
|
||||
userData.PantsColor = PantsColor
|
||||
userData.HatSpr = HatSpr
|
||||
userData.HairColor = HairColor
|
||||
userData.CapeColor = CapeColor
|
||||
userData.ShoesColor = ShoesColor
|
||||
userData.ShirtColor = ShirtColor
|
||||
userData.SkinColor = SkinColor
|
||||
userData.UpdatedAt = datetime.strftime(datetime.now(), "%Y-%m-%dT%H:%M:%SZ")
|
||||
|
||||
db.user_collection.update_one({"ID":userID}, {"$set": {**userData.dict()} })
|
||||
hook.execute_hooks("player_update", userID=userID)
|
||||
return userData.dict()
|
||||
return HTTPException(500, detail="Server failed to handle login (somehow)")
|
||||
|
||||
|
||||
@app.get("/api/v1/refresh")
|
||||
async def refresh_login(Authorization: Union[str, None] = Header(default=None)):
|
||||
|
@ -106,6 +137,7 @@ async def useruploadcooldown():
|
|||
|
||||
@app.get("/api/v1/map")
|
||||
async def search_for_maps(
|
||||
Authorization: Union[str, None] = Header(default=None),
|
||||
start: int = 0,
|
||||
limit: int = 5,
|
||||
min_diff: float = 0.0,
|
||||
|
@ -121,6 +153,18 @@ async def search_for_maps(
|
|||
admin_show_unlisted: Optional[int] = 0,
|
||||
):
|
||||
"""Search for maps."""
|
||||
if Authorization is not None:
|
||||
authcheck = db.auth_check(Authorization)
|
||||
if not authcheck[0]:
|
||||
raise HTTPException(403,
|
||||
detail="Wrong username or password."
|
||||
+ "\nIf you're using the API directly, "
|
||||
+ "then you can just skip the Authorization")
|
||||
userData = types.User(**authcheck[1])
|
||||
plays = userData.Plays
|
||||
else:
|
||||
userData = None
|
||||
plays = {}
|
||||
|
||||
# query = db.maps_collection.find({ "CreatorId": author_id }).limit(limit)
|
||||
if code:
|
||||
|
@ -138,13 +182,39 @@ async def search_for_maps(
|
|||
for i in query:
|
||||
del i["_id"]
|
||||
del i["MapData"]
|
||||
del i["Played"]
|
||||
del i["Clear"]
|
||||
del i["FullClear"]
|
||||
|
||||
if not i["Listed"] and admin_show_unlisted != 1:
|
||||
continue
|
||||
|
||||
level_tags = i['TagIDs'].split(",")
|
||||
CreatedAt = datetime.strptime(i["CreatedAt"], "%Y-%m-%dT%H:%M:%SZ")
|
||||
|
||||
|
||||
Played = False
|
||||
Clear = False
|
||||
FullClear = False
|
||||
if plays and str(i["ID"]) in plays:
|
||||
play = plays[str(i["ID"])]
|
||||
Played = play.Played
|
||||
Clear = play.Clear
|
||||
FullClear = play.FullClear
|
||||
# if userData:
|
||||
# # Old system that checked if a user has a record, remove
|
||||
# leaderboard = i["Leaderboard"]
|
||||
# for record in leaderboard:
|
||||
# record = types.MapLeaderboard(**record)
|
||||
# if record.UserID == userData.ID:
|
||||
# Played = True
|
||||
# Clear = True
|
||||
# if record.FullClear:
|
||||
# FullClear = True
|
||||
# break
|
||||
# print(f"DBG: {i}, {leaderboard[i]}\t{leaderboard[i]['UserID'] == int(userID):}")
|
||||
# if leaderboard[i]["UserID"] == int(userID) and (BestTime is None or leaderboard[i]["BestPlaytime"] < BestTime):
|
||||
# replayIndex = i
|
||||
|
||||
# TODO: Improve tag filtering
|
||||
required_tags_included = False if len(required_tag_list) != 0 else True
|
||||
disallowed_tags_included = False
|
||||
|
@ -154,7 +224,7 @@ async def search_for_maps(
|
|||
else:
|
||||
if required_tags_included and not disallowed_tags_included:
|
||||
if not last_x_hours or CreatedAt.hour < last_x_hours:
|
||||
entries.append(i)
|
||||
entries.append(types.Map(Played=Played, Clear=Clear, FullClear=FullClear, **i))
|
||||
return entries
|
||||
|
||||
|
||||
|
@ -162,7 +232,7 @@ async def search_for_maps(
|
|||
async def getMap(mapID: int):
|
||||
query = db.maps_collection.find_one({"ID": mapID})
|
||||
del query["_id"]
|
||||
return query
|
||||
return types.Map(**query)
|
||||
|
||||
|
||||
@app.post("/api/v1/map/{mapID}/start")
|
||||
|
@ -171,20 +241,35 @@ async def startMap(
|
|||
Authorization: Union[str, None] = Header(default=None)
|
||||
):
|
||||
authcheck = db.auth_check(Authorization)
|
||||
if not authcheck[0] and authcheck[1] == "nouser":
|
||||
raise HTTPException(404, detail="User not found")
|
||||
elif not authcheck[0] and authcheck[1] == "wrongpass":
|
||||
raise HTTPException(403, detail="Wrong password")
|
||||
if not authcheck[0] and authcheck[1]:
|
||||
raise HTTPException(403, detail="Wrong username or password.")
|
||||
elif authcheck[0]:
|
||||
userData = types.User(**authcheck[1])
|
||||
query = db.maps_collection.find_one({"ID": mapID})
|
||||
del query["_id"]
|
||||
Clear = False
|
||||
Played = False
|
||||
if str(mapID) not in userData.Plays :
|
||||
updateQuery = db.user_collection.update_one(
|
||||
{"ID": userData.ID},
|
||||
{
|
||||
"$set": {
|
||||
f"Plays.{mapID}": dict(types.MapPlay())
|
||||
}
|
||||
}
|
||||
)
|
||||
else:
|
||||
play = userData.Plays[str(mapID)]
|
||||
Clear = play.Clear
|
||||
Played = play.Played
|
||||
FullClear = play.FullClear
|
||||
|
||||
|
||||
returned_resp = {
|
||||
"BestDeaths": 0,
|
||||
"BestPlaytime": 0,
|
||||
"Clear": False,
|
||||
"CurMap": query,
|
||||
"CurMap": types.Map(**query),
|
||||
"Difficulty": 0,
|
||||
"Followed": False,
|
||||
"Played": True,
|
||||
|
@ -203,21 +288,29 @@ async def stopMapPlay(
|
|||
playtime: int = Form(),
|
||||
totalDeaths: int = Form(),
|
||||
totalTime: int = Form(),
|
||||
fullClear: 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":
|
||||
raise HTTPException(404, detail="User not found")
|
||||
elif not authcheck[0] and authcheck[1] == "wrongpass":
|
||||
raise HTTPException(403, detail="Wrong password")
|
||||
if not authcheck[0]:
|
||||
raise HTTPException(403, detail="Wrong username or password")
|
||||
elif authcheck[0]:
|
||||
NewMapRecord = False
|
||||
FirstClear = False
|
||||
|
||||
userData = types.User(**authcheck[1])
|
||||
|
||||
userUpdateQuery = db.user_collection.update_one(
|
||||
{"ID": userData.ID},
|
||||
{
|
||||
"$set": {
|
||||
f"Plays.{mapID}": dict(types.MapPlay(Clear=bool(clear), FullClear=bool(fullClear)))
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
query = db.maps_collection.find_one(
|
||||
{"ID": mapID, "Leaderboard.UserID": userData.ID}
|
||||
)
|
||||
|
@ -226,6 +319,7 @@ async def stopMapPlay(
|
|||
|
||||
BestUserTime = None
|
||||
BestTime = None
|
||||
BestFullTime = None
|
||||
if query is not None and "Leaderboard" in query:
|
||||
for i in query["Leaderboard"]:
|
||||
if i["UserID"] == userData.ID:
|
||||
|
@ -233,9 +327,12 @@ async def stopMapPlay(
|
|||
BestUserTime = i["BestPlaytime"]
|
||||
if BestTime is None or BestTime > i["BestPlaytime"]:
|
||||
BestTime = i["BestPlaytime"]
|
||||
elif BestFullTime is None or BestFullTime > i["BestFullTimePlaytime"]:
|
||||
BestFullTime = i["BestFullTimePlaytime"]
|
||||
|
||||
if len(query["Leaderboard"]) <= 1 and i["UserID"] != userData.ID:
|
||||
FirstClear = True
|
||||
if BestUserTime is None or playtime < BestUserTime:
|
||||
if (BestTime is None or playtime < BestTime) or (BestFullTime is None or playtime < BestFullTime):
|
||||
updateQuery = db.maps_collection.update_one(
|
||||
{"ID": mapID},
|
||||
{"$pull": {"Leaderboard": {"UserID": userData.ID}},
|
||||
|
@ -271,11 +368,12 @@ async def stopMapPlay(
|
|||
BestReplay=replayData,
|
||||
CreatorName=userData.Username,
|
||||
UserID=userData.ID,
|
||||
FullClear=bool(fullClear)
|
||||
).dict()
|
||||
},
|
||||
},
|
||||
)
|
||||
if BestTime is None or playtime < BestTime:
|
||||
if (BestTime is None or playtime < BestTime) or (BestFullTime is None or playtime < BestFullTime):
|
||||
NewMapRecord = True
|
||||
|
||||
hook.execute_hooks(
|
||||
|
@ -285,6 +383,7 @@ async def stopMapPlay(
|
|||
clear=clear,
|
||||
deaths=deaths,
|
||||
playtime=playtime,
|
||||
fullClear=fullClear,
|
||||
FirstClear=FirstClear,
|
||||
NewMapRecord=NewMapRecord,
|
||||
)
|
||||
|
@ -308,15 +407,16 @@ async def upload_map(
|
|||
listed: int = Form(),
|
||||
requiresCancels: int = Form(),
|
||||
hideInChallenges: int = Form(),
|
||||
tags: str = Form(),
|
||||
tags: str = Form(default=""),
|
||||
rng: int = Form(),
|
||||
numSubmaps: int = Form(),
|
||||
dragonCoins: int = Form(),
|
||||
replayVisibility: 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")
|
||||
elif not authcheck[0] and authcheck[1] == "wrongpass":
|
||||
raise HTTPException(403, detail="Wrong password")
|
||||
if not authcheck[0]:
|
||||
raise HTTPException(403, detail="Wrong username or password")
|
||||
elif authcheck[0]:
|
||||
print(authcheck)
|
||||
userData = types.User(**authcheck[1])
|
||||
|
@ -454,13 +554,21 @@ async def rateMap(mapID: int, Rating: Rating, Authorization: Union[str, None] =
|
|||
}
|
||||
|
||||
@app.get("/api/v1/map/{mapID}/besttimes/{maxEntries}")
|
||||
async def getMapLeaderboard(mapID: int, maxEntries: int = 5):
|
||||
async def getMapLeaderboard(mapID: int, maxEntries: int = 5, full_clear: int = 0):
|
||||
"""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"]
|
||||
leaderboard = []
|
||||
for record in query["Leaderboard"]:
|
||||
record = types.MapLeaderboard(**record)
|
||||
if record.FullClear and full_clear == 1:
|
||||
leaderboard.append(dict(record))
|
||||
elif not record.FullClear and full_clear == 0:
|
||||
leaderboard.append(dict(record))
|
||||
else:
|
||||
leaderboard.append(dict(record))
|
||||
|
||||
return sorted(leaderboard, key=lambda sort: sort["BestPlaytime"])[0:maxEntries]
|
||||
|
||||
|
@ -513,9 +621,7 @@ async def invalidateAllTimes(
|
|||
|
||||
authcheck = db.auth_check(Authorization)
|
||||
if not authcheck[0] and authcheck[1] == "nouser":
|
||||
raise HTTPException(404, detail="User not found")
|
||||
elif not authcheck[0] and authcheck[1] == "wrongpass":
|
||||
raise HTTPException(403, detail="Wrong password")
|
||||
raise HTTPException(403, detail="Wrong username or password")
|
||||
elif authcheck[0]:
|
||||
userData = types.User(**authcheck[1])
|
||||
if userData.Admin:
|
||||
|
@ -562,9 +668,7 @@ async def reportContent(
|
|||
|
||||
authcheck = db.auth_check(Authorization)
|
||||
if not authcheck[0] and authcheck[1] == "nouser":
|
||||
raise HTTPException(404, detail="User not found")
|
||||
elif not authcheck[0] and authcheck[1] == "wrongpass":
|
||||
raise HTTPException(403, detail="Wrong password")
|
||||
raise HTTPException(403, detail="Wrong user or password")
|
||||
elif authcheck[0]:
|
||||
db.reports_collection.insert_one(
|
||||
{
|
||||
|
@ -590,6 +694,17 @@ async def followcheck():
|
|||
# FIXME: Stub
|
||||
return 0
|
||||
|
||||
@app.post("/api/v1/unlockachievement", response_class=PlainTextResponse)
|
||||
async def unlockachievement(
|
||||
Authorization: Union[str, None] = Header(default=None),
|
||||
achievementId: int = Form()
|
||||
):
|
||||
"""
|
||||
Adds an achivement to the user
|
||||
STUB
|
||||
"""
|
||||
return "Success"
|
||||
|
||||
def start():
|
||||
"""Launched with `poetry run start` at root level"""
|
||||
from . import __main__
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
services:
|
||||
backend:
|
||||
build: .
|
||||
# restart: on-failure
|
||||
ports:
|
||||
- "8001:8001"
|
||||
volumes:
|
||||
|
@ -15,7 +16,7 @@ services:
|
|||
|
||||
mongo:
|
||||
image: mongo
|
||||
restart: always
|
||||
# restart: on-failure
|
||||
expose:
|
||||
- "27017:27017"
|
||||
environment:
|
||||
|
@ -26,7 +27,7 @@ services:
|
|||
|
||||
mongo-express:
|
||||
image: mongo-express
|
||||
restart: always
|
||||
# restart: on-failure
|
||||
ports:
|
||||
- 8081:8081
|
||||
environment:
|
||||
|
|
Loading…
Reference in a new issue