Update server to work with the latest version :3

This commit is contained in:
magmaus3 2023-07-06 17:36:55 +02:00
parent d73c7e44d5
commit a7b9b5b87f
Signed by: magmaus3
GPG key ID: 966755D3F4A9B251
6 changed files with 186 additions and 46 deletions

View file

@ -1,4 +1,7 @@
FROM python:3.11 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 WORKDIR /app
@ -6,4 +9,4 @@ COPY . .
RUN --mount=type=cache,target=/tmp/pip pip install . --cache-dir /tmp/pip 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

View file

@ -2,10 +2,19 @@ from pydantic import BaseModel
from typing import List, Optional, Union from typing import List, Optional, Union
from datetime import datetime 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): class User(BaseModel):
"""Pydantic model for user data. """Pydantic model for user data.
Created from the user response (`/api/v1/user/x`), 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 Admin: bool = True
Banned: bool = False Banned: bool = False
@ -55,7 +64,12 @@ class User(BaseModel):
# Ratings have to be stored in the following format: {mapID: rating}, # Ratings have to be stored in the following format: {mapID: rating},
# where rating is either 1 or 5, or None. # 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): class Notification(BaseModel):
@ -160,7 +174,8 @@ class Map(BaseModel):
MapCode: str = "AAABBAAA" MapCode: str = "AAABBAAA"
Listed: bool = True Listed: bool = True
HiddenInChallenges: bool = False HiddenInChallenges: bool = False
DragonCoins: bool = False ReplayVisibility: int = 0
DragonCoins: bool = False # Gems
# Might be duplicates from the User data # Might be duplicates from the User data
@ -216,6 +231,10 @@ class Map(BaseModel):
BestTimePlaytime: int = 0 BestTimePlaytime: int = 0
MyBestPlaytime: int = 0 MyBestPlaytime: int = 0
BestFullTimeUserID: int = 0
BestFullTimeUsername: str = ""
BestFullTimePlaytime: int = 0
FirstClearUserID: Union[int, None] = 0 FirstClearUserID: Union[int, None] = 0
# Propably not needed, because you can simply read the # Propably not needed, because you can simply read the
@ -240,6 +259,7 @@ class MapLeaderboard(BaseModel):
BestPlaytime: int BestPlaytime: int
BestPlaytimeTime: str = datetime.strftime(datetime.now(), "%Y-%m-%dT%H:%M:%SZ") BestPlaytimeTime: str = datetime.strftime(datetime.now(), "%Y-%m-%dT%H:%M:%SZ")
BestReplay: str BestReplay: str
FullClear: bool = False
CreatorName: str CreatorName: str
UserID: int UserID: int

View file

@ -1,4 +1,5 @@
from datetime import datetime from datetime import datetime
from typing import Literal
from pymongo import MongoClient 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. """Checks credentials.
Returns a tuple with result (for example False, "nouser"). Returns a tuple with result (for example False, "nouser").
Results: Results:
- nouser = user not found - False if wrong username or password
- wrongpass = wrong password - True, [dict] if correct
- [dictionary] = query
""" """
# 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: if Authorization is None:
return False, "noauth" return False, "noauth"
username, password = Authorization.split(":") username, password = Authorization.split(":")
query = user_collection.find_one({"Username": username}) query = user_collection.find_one({"Username": username})
if not query: if not query:
return False, "nouser" return False, "noauth"
if query["Password"] != password: # if query["Password"] != password:
return False, "wrongpass" # return False, "wrongpass"
return True, query return True, query

View file

@ -1,5 +1,4 @@
# from . import * # from . import *
import pkgutil import pkgutil
__path__ = pkgutil.extend_path(__path__, __name__) __path__ = pkgutil.extend_path(__path__, __name__)

View file

@ -38,10 +38,7 @@ async def login(username: str = Form(), password: str = Form(), version: str = F
hook.execute_hooks("player_login", username=username) hook.execute_hooks("player_login", username=username)
auth = db.auth_check(username + ":" + password) auth = db.auth_check(username + ":" + password)
if not auth[0]: if not auth[0]:
if auth[1] == "nouser": raise HTTPException(403, detail="Wrong username or password.")
raise HTTPException(404, detail="User not found.")
elif auth[1] == "wrongpass":
raise HTTPException(403, detail="Password is incorrect.")
else: else:
return {"token": username + ":" + password, "userId": auth[1]["ID"]} return {"token": username + ":" + password, "userId": auth[1]["ID"]}
@ -72,9 +69,43 @@ async def create_user(
@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.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") @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)):
@ -106,6 +137,7 @@ async def useruploadcooldown():
@app.get("/api/v1/map") @app.get("/api/v1/map")
async def search_for_maps( async def search_for_maps(
Authorization: Union[str, None] = Header(default=None),
start: int = 0, start: int = 0,
limit: int = 5, limit: int = 5,
min_diff: float = 0.0, min_diff: float = 0.0,
@ -121,6 +153,18 @@ async def search_for_maps(
admin_show_unlisted: Optional[int] = 0, admin_show_unlisted: Optional[int] = 0,
): ):
"""Search for maps.""" """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) # query = db.maps_collection.find({ "CreatorId": author_id }).limit(limit)
if code: if code:
@ -138,13 +182,39 @@ async def search_for_maps(
for i in query: for i in query:
del i["_id"] del i["_id"]
del i["MapData"] del i["MapData"]
del i["Played"]
del i["Clear"]
del i["FullClear"]
if not i["Listed"] and admin_show_unlisted != 1: if not i["Listed"] and admin_show_unlisted != 1:
continue continue
level_tags = i['TagIDs'].split(",") level_tags = i['TagIDs'].split(",")
CreatedAt = datetime.strptime(i["CreatedAt"], "%Y-%m-%dT%H:%M:%SZ") 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 # TODO: Improve tag filtering
required_tags_included = False if len(required_tag_list) != 0 else True required_tags_included = False if len(required_tag_list) != 0 else True
disallowed_tags_included = False disallowed_tags_included = False
@ -154,7 +224,7 @@ async def search_for_maps(
else: else:
if required_tags_included and not disallowed_tags_included: if required_tags_included and not disallowed_tags_included:
if not last_x_hours or CreatedAt.hour < last_x_hours: 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 return entries
@ -162,7 +232,7 @@ async def search_for_maps(
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 types.Map(**query)
@app.post("/api/v1/map/{mapID}/start") @app.post("/api/v1/map/{mapID}/start")
@ -171,20 +241,35 @@ async def startMap(
Authorization: Union[str, None] = Header(default=None) Authorization: Union[str, None] = Header(default=None)
): ):
authcheck = db.auth_check(Authorization) authcheck = db.auth_check(Authorization)
if not authcheck[0] and authcheck[1] == "nouser": if not authcheck[0] and authcheck[1]:
raise HTTPException(404, detail="User not found") raise HTTPException(403, detail="Wrong username or password.")
elif not authcheck[0] and authcheck[1] == "wrongpass":
raise HTTPException(403, detail="Wrong password")
elif authcheck[0]: elif authcheck[0]:
userData = types.User(**authcheck[1]) userData = types.User(**authcheck[1])
query = db.maps_collection.find_one({"ID": mapID}) query = db.maps_collection.find_one({"ID": mapID})
del query["_id"] 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 = { returned_resp = {
"BestDeaths": 0, "BestDeaths": 0,
"BestPlaytime": 0, "BestPlaytime": 0,
"Clear": False, "Clear": False,
"CurMap": query, "CurMap": types.Map(**query),
"Difficulty": 0, "Difficulty": 0,
"Followed": False, "Followed": False,
"Played": True, "Played": True,
@ -203,21 +288,29 @@ async def stopMapPlay(
playtime: int = Form(), playtime: int = Form(),
totalDeaths: int = Form(), totalDeaths: int = Form(),
totalTime: int = Form(), totalTime: int = Form(),
fullClear: int = Form(),
replayData: str = Form(), replayData: str = Form(),
Authorization: Union[str, None] = Header(default=None), Authorization: Union[str, None] = Header(default=None),
): ):
"""Saves the map replay, and informs the user if their play is a record""" """Saves the map replay, and informs the user if their play is a record"""
authcheck = db.auth_check(Authorization) authcheck = db.auth_check(Authorization)
if not authcheck[0] and authcheck[1] == "nouser": if not authcheck[0]:
raise HTTPException(404, detail="User not found") raise HTTPException(403, detail="Wrong username or password")
elif not authcheck[0] and authcheck[1] == "wrongpass":
raise HTTPException(403, detail="Wrong password")
elif authcheck[0]: elif authcheck[0]:
NewMapRecord = False NewMapRecord = False
FirstClear = False FirstClear = False
userData = types.User(**authcheck[1]) 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( query = db.maps_collection.find_one(
{"ID": mapID, "Leaderboard.UserID": userData.ID} {"ID": mapID, "Leaderboard.UserID": userData.ID}
) )
@ -226,6 +319,7 @@ async def stopMapPlay(
BestUserTime = None BestUserTime = None
BestTime = None BestTime = None
BestFullTime = None
if query is not None and "Leaderboard" in query: if query is not None and "Leaderboard" in query:
for i in query["Leaderboard"]: for i in query["Leaderboard"]:
if i["UserID"] == userData.ID: if i["UserID"] == userData.ID:
@ -233,9 +327,12 @@ async def stopMapPlay(
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"]
elif BestFullTime is None or BestFullTime > i["BestFullTimePlaytime"]:
BestFullTime = i["BestFullTimePlaytime"]
if len(query["Leaderboard"]) <= 1 and i["UserID"] != userData.ID: if len(query["Leaderboard"]) <= 1 and i["UserID"] != userData.ID:
FirstClear = True 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( updateQuery = db.maps_collection.update_one(
{"ID": mapID}, {"ID": mapID},
{"$pull": {"Leaderboard": {"UserID": userData.ID}}, {"$pull": {"Leaderboard": {"UserID": userData.ID}},
@ -271,11 +368,12 @@ async def stopMapPlay(
BestReplay=replayData, BestReplay=replayData,
CreatorName=userData.Username, CreatorName=userData.Username,
UserID=userData.ID, UserID=userData.ID,
FullClear=bool(fullClear)
).dict() ).dict()
}, },
}, },
) )
if BestTime is None or playtime < BestTime: if (BestTime is None or playtime < BestTime) or (BestFullTime is None or playtime < BestFullTime):
NewMapRecord = True NewMapRecord = True
hook.execute_hooks( hook.execute_hooks(
@ -285,6 +383,7 @@ async def stopMapPlay(
clear=clear, clear=clear,
deaths=deaths, deaths=deaths,
playtime=playtime, playtime=playtime,
fullClear=fullClear,
FirstClear=FirstClear, FirstClear=FirstClear,
NewMapRecord=NewMapRecord, NewMapRecord=NewMapRecord,
) )
@ -308,15 +407,16 @@ async def upload_map(
listed: int = Form(), listed: int = Form(),
requiresCancels: int = Form(), requiresCancels: int = Form(),
hideInChallenges: int = Form(), hideInChallenges: int = Form(),
tags: str = Form(), tags: str = Form(default=""),
rng: int = Form(), rng: int = Form(),
numSubmaps: int = Form(),
dragonCoins: int = Form(),
replayVisibility: int = Form(),
clientVersion: float = Form(), clientVersion: float = Form(),
): ):
authcheck = db.auth_check(Authorization) authcheck = db.auth_check(Authorization)
if not authcheck[0] and authcheck[1] == "nouser": if not authcheck[0]:
raise HTTPException(404, detail="User not found") raise HTTPException(403, detail="Wrong username or password")
elif not authcheck[0] and authcheck[1] == "wrongpass":
raise HTTPException(403, detail="Wrong password")
elif authcheck[0]: elif authcheck[0]:
print(authcheck) print(authcheck)
userData = types.User(**authcheck[1]) 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}") @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""" """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 = []
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] return sorted(leaderboard, key=lambda sort: sort["BestPlaytime"])[0:maxEntries]
@ -513,9 +621,7 @@ async def invalidateAllTimes(
authcheck = db.auth_check(Authorization) authcheck = db.auth_check(Authorization)
if not authcheck[0] and authcheck[1] == "nouser": if not authcheck[0] and authcheck[1] == "nouser":
raise HTTPException(404, detail="User not found") raise HTTPException(403, detail="Wrong username or password")
elif not authcheck[0] and authcheck[1] == "wrongpass":
raise HTTPException(403, detail="Wrong password")
elif authcheck[0]: elif authcheck[0]:
userData = types.User(**authcheck[1]) userData = types.User(**authcheck[1])
if userData.Admin: if userData.Admin:
@ -562,9 +668,7 @@ async def reportContent(
authcheck = db.auth_check(Authorization) authcheck = db.auth_check(Authorization)
if not authcheck[0] and authcheck[1] == "nouser": if not authcheck[0] and authcheck[1] == "nouser":
raise HTTPException(404, detail="User not found") raise HTTPException(403, detail="Wrong user or password")
elif not authcheck[0] and authcheck[1] == "wrongpass":
raise HTTPException(403, detail="Wrong password")
elif authcheck[0]: elif authcheck[0]:
db.reports_collection.insert_one( db.reports_collection.insert_one(
{ {
@ -590,6 +694,17 @@ async def followcheck():
# FIXME: Stub # FIXME: Stub
return 0 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(): def start():
"""Launched with `poetry run start` at root level""" """Launched with `poetry run start` at root level"""
from . import __main__ from . import __main__

View file

@ -1,6 +1,7 @@
services: services:
backend: backend:
build: . build: .
# restart: on-failure
ports: ports:
- "8001:8001" - "8001:8001"
volumes: volumes:
@ -15,7 +16,7 @@ services:
mongo: mongo:
image: mongo image: mongo
restart: always # restart: on-failure
expose: expose:
- "27017:27017" - "27017:27017"
environment: environment:
@ -26,7 +27,7 @@ services:
mongo-express: mongo-express:
image: mongo-express image: mongo-express
restart: always # restart: on-failure
ports: ports:
- 8081:8081 - 8081:8081
environment: environment: