Compare commits

..

10 Commits

Author SHA1 Message Date
c0d608b63f Added save sorting by upload and progress 2023-01-26 11:25:42 +01:00
dd7031ff19 Changed DT method to fix UTC time 2023-01-25 15:59:54 +01:00
1294d1a037 Changed only_ids behavior + sorting added 2023-01-23 16:39:11 +01:00
546600a29e Changed only_ids response (still not final) 2023-01-23 10:36:28 +01:00
Profitroll
30f11a7c83 Added client arg to PATCH /devices/{name} 2023-01-22 22:35:33 +01:00
Profitroll
14d1ba9fa7 Added only_ids to GET /saves 2023-01-22 11:31:00 +01:00
Profitroll
ce768d895d Added getting the last save 2023-01-21 18:02:36 +01:00
daa3b0ca73 Added device client option 2023-01-20 16:02:46 +01:00
673c986ff9 Typo fixed 2023-01-19 22:15:07 +02:00
453c3e95dd Argument for /check fixed 2023-01-19 22:10:04 +02:00
8 changed files with 39 additions and 26 deletions

View File

@@ -30,6 +30,7 @@ async def devices_get_by_name(name: str, apikey: APIKey = Depends(get_api_key)):
device = col_devices.find_one({"user": user_by_key(apikey), "name": name})
if device is not None:
del device["_id"]
del device["user"]
return UJSONResponse(device)
else:
raise HTTPException(HTTP_404_NOT_FOUND, detail="Could not find device with that name.")
@@ -46,7 +47,7 @@ async def devices_delete_by_name(name: str, apikey: APIKey = Depends(get_api_key
raise HTTPException(HTTP_404_NOT_FOUND, detail="Could not find device with that name.")
@app.post("/devices", response_class=UJSONResponse, description="Create new device")
async def devices_post(name: str, os: str, apikey: APIKey = Depends(get_api_key)):
async def devices_post(name: str, os: str, client: str, apikey: APIKey = Depends(get_api_key)):
user = user_by_key(apikey)
@@ -62,12 +63,12 @@ async def devices_post(name: str, os: str, apikey: APIKey = Depends(get_api_key)
if col_devices.find_one({"user": user, "name": name}) is not None:
raise HTTPException(HTTP_409_CONFLICT, detail="Device with this name already exists.")
col_devices.insert_one({"user": user, "name": name, "os": os, "last_save": 0})
col_devices.insert_one({"user": user, "name": name, "os": os, "client": client, "last_save": 0})
return Response(status_code=HTTP_204_NO_CONTENT)
@app.patch("/devices/{name}", description="Update name of the existing device")
async def devices_patch(name: str, new_name: str, os: str, apikey: APIKey = Depends(get_api_key)):
async def devices_patch(name: str, new_name: str, os: str, client: str, apikey: APIKey = Depends(get_api_key)):
user = user_by_key(apikey)
@@ -77,7 +78,7 @@ async def devices_patch(name: str, new_name: str, os: str, apikey: APIKey = Depe
if col_devices.find_one({"user": user, "name": new_name}) is not None:
raise HTTPException(HTTP_409_CONFLICT, detail="Device with this name already exists.")
col_devices.find_one_and_update({"user": user, "name": name}, {"$set": {"name": new_name, "os": os}})
col_devices.find_one_and_update({"user": user, "name": name}, {"$set": {"name": new_name, "os": os, "client": client}})
col_saves.update_many({"user": user, "device": name}, {"$set": {"device": new_name}})
return Response(status_code=HTTP_204_NO_CONTENT)

View File

@@ -1,9 +1,10 @@
from datetime import datetime
from datetime import datetime, timezone
from urllib.parse import quote_plus
from os import remove
from typing import Dict, List, Union
from typing import Dict, List, Literal, Union
from xmltodict import parse
from models.saves import StardewSave
from pymongo import DESCENDING
from models.saves import StardewSave, StardewSaveBrief
from modules.app import app, get_api_key, user_by_key
from modules.database import col_devices, col_saves
from fastapi import HTTPException, Depends, UploadFile
@@ -14,8 +15,8 @@ from starlette.status import HTTP_204_NO_CONTENT, HTTP_403_FORBIDDEN, HTTP_404_N
from modules.utils import configGet, zip_saves
@app.get("/saves", response_class=UJSONResponse, response_model=Dict[str, StardewSave], description="Get all available game saves")
async def saves_get(device: Union[str, None] = None, version: Union[str, None] = None, apikey: APIKey = Depends(get_api_key)):
@app.get("/saves", response_class=UJSONResponse, response_model=Union[List[Dict[str, StardewSave]], List[StardewSaveBrief]], description="Get all available game saves")
async def saves_get(device: Union[str, None] = None, version: Union[str, None] = None, only_ids: bool = False, sort: Union[Literal["upload", "progress"], None] = "upload", apikey: APIKey = Depends(get_api_key)):
user = user_by_key(apikey)
@@ -27,25 +28,31 @@ async def saves_get(device: Union[str, None] = None, version: Union[str, None] =
if version is not None:
query["data.game_version"] = version
saves_entries = list(col_saves.find(query))
saves_entries = list(col_saves.find(query).sort("date", DESCENDING)) if sort == "upload" else list(col_saves.find(query).sort("data.save_time", DESCENDING))
if len(saves_entries) == 0:
raise HTTPException(HTTP_404_NOT_FOUND, detail="Could not find any saves.")
output = []
added = []
for entry in saves_entries:
out_entry = entry
del out_entry["_id"]
del out_entry["user"]
del out_entry["file"]
if only_ids is True:
if entry["id"] in added:
continue
else:
added.append(entry["id"])
output.append(out_entry)
return UJSONResponse(output)
@app.get("/saves/{id}", response_class=UJSONResponse, response_model=List[StardewSave], description="Get game saves by name")
async def saves_get_by_id(id: int, device: Union[str, None] = None, version: Union[str, None] = None, apikey: APIKey = Depends(get_api_key)):
async def saves_get_by_id(id: int, device: Union[str, None] = None, version: Union[str, None] = None, sort: Union[Literal["upload", "progress"], None] = "upload", apikey: APIKey = Depends(get_api_key)):
query = {"id": id}
@@ -55,7 +62,7 @@ async def saves_get_by_id(id: int, device: Union[str, None] = None, version: Uni
if version is not None:
query["data.game_version"] = version
saves_entries = list(col_saves.find(query))
saves_entries = list(col_saves.find(query).sort("date", DESCENDING)) if sort == "upload" else list(col_saves.find(query).sort("data.save_time", DESCENDING))
if len(saves_entries) == 0:
raise HTTPException(HTTP_404_NOT_FOUND, detail="Could not find save with such id.")
@@ -72,8 +79,8 @@ async def saves_get_by_id(id: int, device: Union[str, None] = None, version: Uni
return UJSONResponse(output)
@app.get("/saves/{id}/{save_date}", response_class=UJSONResponse, response_model=List[StardewSave], description="Get game saves by name")
async def saves_get_by_both_ids(id: int, save_date: int, device: Union[str, None] = None, apikey: APIKey = Depends(get_api_key)):
@app.get("/saves/{id}/{save_date}", response_class=UJSONResponse, response_model=List[StardewSave], description="Get game saves by id and upload date")
async def saves_get_by_both_ids(id: int, save_date: Union[int, Literal["latest"]], device: Union[str, None] = None, apikey: APIKey = Depends(get_api_key)):
query = {"user": user_by_key(apikey), "id": id, "date": save_date}
@@ -90,7 +97,7 @@ async def saves_get_by_both_ids(id: int, save_date: int, device: Union[str, None
raise HTTPException(HTTP_404_NOT_FOUND, detail="Could not find save with such id.")
@app.delete("/saves/{id}", description="Get game saves by name")
@app.delete("/saves/{id}", description="Delete game saves by id")
async def saves_delete_by_id(id: int, apikey: APIKey = Depends(get_api_key)):
user = user_by_key(apikey)
if col_saves.count_documents({"user": user, "id": id}) > 0:
@@ -103,7 +110,7 @@ async def saves_delete_by_id(id: int, apikey: APIKey = Depends(get_api_key)):
raise HTTPException(HTTP_404_NOT_FOUND, detail="Could not find save with such id.")
@app.delete("/saves/{id}/{save_date}", response_class=UJSONResponse, description="Get game saves by name")
@app.delete("/saves/{id}/{save_date}", response_class=UJSONResponse, description="Delete game saves by id and upload date")
async def saves_delete_by_both_ids(id: int, save_date: int, apikey: APIKey = Depends(get_api_key)):
saves_entry = col_saves.find_one_and_delete({"id": id, "date": save_date})
if saves_entry is not None:
@@ -113,7 +120,7 @@ async def saves_delete_by_both_ids(id: int, save_date: int, apikey: APIKey = Dep
raise HTTPException(HTTP_404_NOT_FOUND, detail="Could not find save with such id.")
@app.get("/saves/{id}/{save_date}/download", response_class=FileResponse, description="Get game save as .svsave file by its id and save date")
@app.get("/saves/{id}/{save_date}/download", response_class=FileResponse, description="Get game save as .svsave file by its id and upload date")
async def saves_download(id: int, save_date: int, device: Union[str, None] = None, apikey: APIKey = Depends(get_api_key)):
saves_entry = col_saves.find_one({"user": user_by_key(apikey), "id": id, "date": save_date}) if device is None else col_saves.find_one({"user": user_by_key(apikey), "id": id, "device": device, "date": save_date})
if saves_entry is not None: # type: ignore
@@ -175,7 +182,7 @@ async def saves_post(device: str, files: List[UploadFile], apikey: APIKey = Depe
zipped = zip_saves(save_data_filename, save_data_file, save_info_file)
save_date = int(datetime.utcnow().timestamp())
save_date = int(datetime.now(tz=timezone.utc).timestamp())
index = {
"id": int(save_data["SaveGame"]["uniqueIDForThisGame"]),

View File

@@ -4,4 +4,5 @@ class Device(BaseModel):
user: str
name: str
os: str
client: str
last_save: int

View File

@@ -14,4 +14,8 @@ class StardewSave(BaseModel):
id: int
device: str
date: int
data: StardewSaveData
data: StardewSaveData
class StardewSaveBrief(BaseModel):
id: int
farmer: str

View File

@@ -13,13 +13,11 @@ app = FastAPI(title="Stardew Sync", docs_url=None, redoc_url=None, version="0.1"
api_key_query = APIKeyQuery(name="apikey", auto_error=False)
api_key_header = APIKeyHeader(name="apikey", auto_error=False)
api_key_cookie = APIKeyCookie(name="apikey", auto_error=False)
async def get_api_key(
api_key_query: str = Security(api_key_query),
api_key_header: str = Security(api_key_header),
api_key_cookie: str = Security(api_key_cookie),
) -> str:
def is_valid(key):
@@ -29,10 +27,8 @@ async def get_api_key(
return api_key_query
elif is_valid(api_key_header):
return api_key_header
elif is_valid(api_key_cookie):
return api_key_cookie
else:
if (col_expired.find_one({"hash": passEncode(api_key_query)}) is not None) or (col_expired.find_one({"hash": passEncode(api_key_header)}) is not None) or (col_expired.find_one({"hash": passEncode(api_key_cookie)}) is not None):
if (col_expired.find_one({"hash": passEncode(api_key_query)}) is not None) or (col_expired.find_one({"hash": passEncode(api_key_header)}) is not None):
raise HTTPException(status_code=HTTP_403_FORBIDDEN, detail=configGet("key_expired", "messages"))
else:
raise HTTPException(status_code=HTTP_401_UNAUTHORIZED, detail=configGet("key_invalid", "messages"))

View File

@@ -9,7 +9,7 @@ makedirs(configGet("data", "locations"), exist_ok=True)
@app.get("/check", response_class=Response, include_in_schema=False)
async def check():
return Response(HTTP_200_OK)
return Response(status_code=HTTP_200_OK)
@app.get("/favicon.ico", response_class=FileResponse, include_in_schema=False)
async def favicon():

View File

@@ -12,7 +12,7 @@ parser = ArgumentParser(
parser.add_argument("-u", "--username", help="Enter username without input prompt", action="store")
parser.add_argument("-e", "--email", help="Enter email without input prompt", action="store")
parser.add_argument("-l", "--local", help="Do not save user's email to make it completely local and unrecoverable", action="store_trues")
parser.add_argument("-l", "--local", help="Do not save user's email to make it completely local and unrecoverable", action="store_true")
parser.add_argument("-j", "--json", help="Return output as a json. Username must be provided as an argument", action="store_true")
args = parser.parse_args()

View File

@@ -4,6 +4,7 @@
"user",
"name",
"os",
"client",
"last_save"
],
"properties": {
@@ -16,6 +17,9 @@
"os": {
"bsonType": "string"
},
"client": {
"bsonType": "string"
},
"last_save": {
"bsonType": ["int", "double"]
}