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}) device = col_devices.find_one({"user": user_by_key(apikey), "name": name})
if device is not None: if device is not None:
del device["_id"] del device["_id"]
del device["user"]
return UJSONResponse(device) return UJSONResponse(device)
else: else:
raise HTTPException(HTTP_404_NOT_FOUND, detail="Could not find device with that name.") 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.") raise HTTPException(HTTP_404_NOT_FOUND, detail="Could not find device with that name.")
@app.post("/devices", response_class=UJSONResponse, description="Create new device") @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) 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: if col_devices.find_one({"user": user, "name": name}) is not None:
raise HTTPException(HTTP_409_CONFLICT, detail="Device with this name already exists.") 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) return Response(status_code=HTTP_204_NO_CONTENT)
@app.patch("/devices/{name}", description="Update name of the existing device") @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) 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: 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.") 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}}) col_saves.update_many({"user": user, "device": name}, {"$set": {"device": new_name}})
return Response(status_code=HTTP_204_NO_CONTENT) 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 urllib.parse import quote_plus
from os import remove from os import remove
from typing import Dict, List, Union from typing import Dict, List, Literal, Union
from xmltodict import parse 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.app import app, get_api_key, user_by_key
from modules.database import col_devices, col_saves from modules.database import col_devices, col_saves
from fastapi import HTTPException, Depends, UploadFile 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 from modules.utils import configGet, zip_saves
@app.get("/saves", response_class=UJSONResponse, response_model=Dict[str, StardewSave], description="Get all available game saves") @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, apikey: APIKey = Depends(get_api_key)): 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) 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: if version is not None:
query["data.game_version"] = version 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: if len(saves_entries) == 0:
raise HTTPException(HTTP_404_NOT_FOUND, detail="Could not find any saves.") raise HTTPException(HTTP_404_NOT_FOUND, detail="Could not find any saves.")
output = [] output = []
added = []
for entry in saves_entries: for entry in saves_entries:
out_entry = entry out_entry = entry
del out_entry["_id"] del out_entry["_id"]
del out_entry["user"] del out_entry["user"]
del out_entry["file"] del out_entry["file"]
if only_ids is True:
if entry["id"] in added:
continue
else:
added.append(entry["id"])
output.append(out_entry) output.append(out_entry)
return UJSONResponse(output) return UJSONResponse(output)
@app.get("/saves/{id}", response_class=UJSONResponse, response_model=List[StardewSave], description="Get game saves by name") @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} 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: if version is not None:
query["data.game_version"] = version 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: if len(saves_entries) == 0:
raise HTTPException(HTTP_404_NOT_FOUND, detail="Could not find save with such id.") 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) return UJSONResponse(output)
@app.get("/saves/{id}/{save_date}", response_class=UJSONResponse, response_model=List[StardewSave], description="Get game saves by name") @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: int, device: Union[str, None] = None, apikey: APIKey = Depends(get_api_key)): 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} 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.") 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)): async def saves_delete_by_id(id: int, apikey: APIKey = Depends(get_api_key)):
user = user_by_key(apikey) user = user_by_key(apikey)
if col_saves.count_documents({"user": user, "id": id}) > 0: 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.") 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)): 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}) saves_entry = col_saves.find_one_and_delete({"id": id, "date": save_date})
if saves_entry is not None: 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.") 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)): 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}) 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 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) 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 = { index = {
"id": int(save_data["SaveGame"]["uniqueIDForThisGame"]), "id": int(save_data["SaveGame"]["uniqueIDForThisGame"]),

View File

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

View File

@@ -15,3 +15,7 @@ class StardewSave(BaseModel):
device: str device: str
date: int 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_query = APIKeyQuery(name="apikey", auto_error=False)
api_key_header = APIKeyHeader(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( async def get_api_key(
api_key_query: str = Security(api_key_query), api_key_query: str = Security(api_key_query),
api_key_header: str = Security(api_key_header), api_key_header: str = Security(api_key_header),
api_key_cookie: str = Security(api_key_cookie),
) -> str: ) -> str:
def is_valid(key): def is_valid(key):
@@ -29,10 +27,8 @@ async def get_api_key(
return api_key_query return api_key_query
elif is_valid(api_key_header): elif is_valid(api_key_header):
return api_key_header return api_key_header
elif is_valid(api_key_cookie):
return api_key_cookie
else: 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")) raise HTTPException(status_code=HTTP_403_FORBIDDEN, detail=configGet("key_expired", "messages"))
else: else:
raise HTTPException(status_code=HTTP_401_UNAUTHORIZED, detail=configGet("key_invalid", "messages")) 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) @app.get("/check", response_class=Response, include_in_schema=False)
async def check(): 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) @app.get("/favicon.ico", response_class=FileResponse, include_in_schema=False)
async def favicon(): 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("-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("-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") 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() args = parser.parse_args()

View File

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