Compare commits

...

20 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
aff3f76fc1 Local account support added 2023-01-19 15:31:40 +01:00
1ce8c0d712 Mailer added 2023-01-19 15:31:27 +01:00
5f8dff42c3 Saves unlim by -1 added 2023-01-19 14:13:14 +01:00
02552dce5a Limits implemented 2023-01-19 14:11:49 +01:00
84be0c7154 Versions added 2023-01-19 13:58:18 +01:00
08d060e160 Refactored some stuff 2023-01-19 13:47:18 +01:00
aa8be6006c Files zipping update 2023-01-19 13:35:27 +01:00
3c245d8671 Removed trash and optimized imports 2023-01-18 14:31:22 +01:00
335a497991 Added device arg into search by both ID 2023-01-18 14:30:14 +01:00
1beca94cc0 Added search by device 2023-01-18 14:27:58 +01:00
12 changed files with 214 additions and 145 deletions

View File

@@ -9,6 +9,11 @@
"locations": { "locations": {
"data": "data" "data": "data"
}, },
"compression": 5,
"limits": {
"saves": -1,
"devices": -1
},
"messages": { "messages": {
"key_expired": "API key expired", "key_expired": "API key expired",
"key_invalid": "Invalid API key", "key_invalid": "Invalid API key",
@@ -19,5 +24,25 @@
"user_already_exists": "User with this username already exists.", "user_already_exists": "User with this username already exists.",
"email_confirmed": "Email confirmed. You can now log in.", "email_confirmed": "Email confirmed. You can now log in.",
"email_code_invalid": "Confirmation code is invalid." "email_code_invalid": "Confirmation code is invalid."
},
"external_address": "localhost",
"registration_enabled": false,
"registration_requires_confirmation": false,
"mailer": {
"smtp": {
"host": "",
"port": 0,
"sender": "",
"login": "",
"password": "",
"use_ssl": true,
"use_tls": false
},
"messages": {
"registration_confirmation": {
"subject": "Email confirmation",
"message": "To confirm your email please follow this link: {0}"
}
}
} }
} }

View File

@@ -3,7 +3,9 @@ from modules.database import col_devices, col_saves
from fastapi import HTTPException, Depends from fastapi import HTTPException, Depends
from fastapi.responses import UJSONResponse, Response from fastapi.responses import UJSONResponse, Response
from fastapi.openapi.models import APIKey from fastapi.openapi.models import APIKey
from starlette.status import HTTP_204_NO_CONTENT, HTTP_404_NOT_FOUND, HTTP_409_CONFLICT from starlette.status import HTTP_204_NO_CONTENT, HTTP_403_FORBIDDEN, HTTP_404_NOT_FOUND, HTTP_409_CONFLICT
from modules.utils import configGet
@app.get("/devices", response_class=UJSONResponse, description="Get all devices") @app.get("/devices", response_class=UJSONResponse, description="Get all devices")
async def devices_get(apikey: APIKey = Depends(get_api_key)): async def devices_get(apikey: APIKey = Depends(get_api_key)):
@@ -28,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.")
@@ -44,19 +47,28 @@ 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)
if (configGet("devices", "limits") != -1) and (col_devices.count_documents({"user": user}) >= configGet("devices", "limits")):
return UJSONResponse(
{
"detail": f'Too many devices. This instance allows to register only {configGet("devices", "limits")} devices per user',
"limit": configGet("devices", "limits")
},
status_code=HTTP_403_FORBIDDEN
)
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)
@@ -66,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,74 +1,68 @@
from datetime import datetime from datetime import datetime, timezone
from io import BytesIO
from urllib.parse import quote_plus from urllib.parse import quote_plus
from os import path, remove from os import remove
from typing import Dict, List, Union from typing import Dict, List, Literal, Union
from zipfile import ZipFile
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.utils import saveFile
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
from fastapi.responses import UJSONResponse, FileResponse, Response from fastapi.responses import UJSONResponse, FileResponse, Response
from fastapi.openapi.models import APIKey from fastapi.openapi.models import APIKey
from starlette.status import HTTP_204_NO_CONTENT, HTTP_404_NOT_FOUND, HTTP_406_NOT_ACCEPTABLE from starlette.status import HTTP_204_NO_CONTENT, HTTP_403_FORBIDDEN, HTTP_404_NOT_FOUND, HTTP_406_NOT_ACCEPTABLE
from modules.utils import configGet, zip_saves
def zipfiles(filenames, save_name: str) -> Response: @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)):
zip_filename = save_name+".svsave" user = user_by_key(apikey)
s = BytesIO() query = {"user": user}
zf = ZipFile(s, "w")
for fpath in filenames: if device is not None:
query["device"] = device
# Calculate path for file in zip if version is not None:
fdir, fname = path.split(fpath) query["data.game_version"] = version
# Add file, at correct path saves_entries = list(col_saves.find(query).sort("date", DESCENDING)) if sort == "upload" else list(col_saves.find(query).sort("data.save_time", DESCENDING))
for entry in (list(col_saves.find({"files.save.uuid": fname})) + list(col_saves.find({"files.saveinfo.uuid": fname}))):
filename = entry["files"]["save"]["name"] if (entry["files"]["save"]["uuid"] == fname) else entry["files"]["saveinfo"]["name"]
zf.write(fpath, filename)
# Must close zip for all contents to be written
zf.close()
# Grab ZIP file from in-memory, make response with correct MIME-type
return Response(
s.getvalue(),
media_type="application/x-zip-compressed",
headers={
'Content-Disposition': f'attachment;filename={quote_plus(zip_filename)}'
}
)
@app.get("/saves", response_class=UJSONResponse, response_model=Dict[str, StardewSave], description="Get all available game saves")
async def saves_get(apikey: APIKey = Depends(get_api_key)):
saves_entries = list(col_saves.find({"user": user_by_key(apikey)}))
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["files"] 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, 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)):
saves_entries = list(col_saves.find({"id": id})) if device is None else list(col_saves.find({"id": id, "device": device})) query = {"id": id}
if device is not None:
query["device"] = device
if version is not None:
query["data.game_version"] = version
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.")
@@ -78,54 +72,67 @@ async def saves_get_by_id(id: int, device: Union[str, None] = None, apikey: APIK
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["files"] del out_entry["file"]
del out_entry["user"] del out_entry["user"]
output.append(out_entry) output.append(out_entry)
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, 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)):
saves_entry = col_saves.find_one({"user": user_by_key(apikey), "id": id, "date": save_date})
query = {"user": user_by_key(apikey), "id": id, "date": save_date}
if device is not None:
query["device"] = device
saves_entry = col_saves.find_one(query)
if saves_entry is not None: if saves_entry is not None:
del saves_entry["_id"] del saves_entry["_id"]
del saves_entry["files"] del saves_entry["file"]
return UJSONResponse(saves_entry) return UJSONResponse(saves_entry)
else: else:
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:
saves_entries = list(col_saves.find({"user": user, "id": id})) saves_entries = list(col_saves.find({"user": user, "id": id}))
for entry in saves_entries: for entry in saves_entries:
remove(entry["files"]["save"]["path"]) remove(entry["file"]["path"])
remove(entry["files"]["saveinfo"]["path"])
col_saves.delete_many({"user": user, "id": id}) col_saves.delete_many({"user": user, "id": id})
return Response(status_code=HTTP_204_NO_CONTENT) return Response(status_code=HTTP_204_NO_CONTENT)
else: else:
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:
remove(saves_entry["files"]["save"]["path"]) remove(saves_entry["file"]["path"])
remove(saves_entry["files"]["saveinfo"]["path"])
return Response(status_code=HTTP_204_NO_CONTENT) return Response(status_code=HTTP_204_NO_CONTENT)
else: else:
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, 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}) 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
return zipfiles([saves_entry["files"]["save"]["path"], saves_entry["files"]["saveinfo"]["path"]], save_name=f'{saves_entry["data"]["farmer"]}_{saves_entry["id"]}') with open(saves_entry["file"]["path"], "rb") as file:
response = Response(
file.read(),
media_type="application/x-zip-compressed",
headers={
'Content-Disposition': f'attachment;filename={quote_plus(saves_entry["file"]["name"])}.svsave'
}
)
return response
else: else:
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.")
@@ -137,6 +144,15 @@ async def saves_post(device: str, files: List[UploadFile], apikey: APIKey = Depe
error_return = HTTPException(HTTP_406_NOT_ACCEPTABLE, detail="You must provide two files: save file and SaveGameInfo for that save") error_return = HTTPException(HTTP_406_NOT_ACCEPTABLE, detail="You must provide two files: save file and SaveGameInfo for that save")
if (configGet("saves", "limits") != -1) and (col_saves.count_documents({"user": user}) >= configGet("saves", "limits")):
return UJSONResponse(
{
"detail": f'Too many save files. This instance allows to store only {configGet("saves", "limits")} saves per user',
"limit": configGet("saves", "limits")
},
status_code=HTTP_403_FORBIDDEN
)
if len(files) != 2: if len(files) != 2:
return error_return return error_return
@@ -146,27 +162,27 @@ async def saves_post(device: str, files: List[UploadFile], apikey: APIKey = Depe
if col_devices.find_one({"user": user, "name": device}) is None: if col_devices.find_one({"user": user, "name": device}) is None:
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.")
save_info = save_data = save_info_file = save_data_filename = save_data_file = save_info_file_id = save_data_file_id = None save_info = save_data = save_info_file = save_data_filename = save_data_file = None
for file in files: for file in files:
if file.filename == "SaveGameInfo": if file.filename == "SaveGameInfo":
save_info_file = await file.read() save_info_file = await file.read()
save_info_file_id = saveFile(save_info_file)
save_info = parse(save_info_file.decode("utf-8")) save_info = parse(save_info_file.decode("utf-8"))
if "Farmer" not in save_info: if "Farmer" not in save_info:
return error_return return error_return
else: else:
save_data_filename = file.filename save_data_filename = file.filename
save_data_file = await file.read() save_data_file = await file.read()
save_data_file_id = saveFile(save_data_file)
save_data = parse(save_data_file.decode("utf-8")) save_data = parse(save_data_file.decode("utf-8"))
if "SaveGame" not in save_data: if "SaveGame" not in save_data:
return error_return return error_return
if save_info is None or save_data is None or save_info_file is None or save_data_filename is None or save_data_file is None or save_info_file_id is None or save_data_file_id is None: if save_info is None or save_data is None or save_info_file is None or save_data_filename is None or save_data_file is None:
return error_return return error_return
save_date = int(datetime.utcnow().timestamp()) zipped = zip_saves(save_data_filename, save_data_file, save_info_file)
save_date = int(datetime.now(tz=timezone.utc).timestamp())
index = { index = {
"id": int(save_data["SaveGame"]["uniqueIDForThisGame"]), "id": int(save_data["SaveGame"]["uniqueIDForThisGame"]),
@@ -180,19 +196,13 @@ async def saves_post(device: str, files: List[UploadFile], apikey: APIKey = Depe
"save_time": int(save_info["Farmer"]["saveTime"]), "save_time": int(save_info["Farmer"]["saveTime"]),
"year": int(save_info["Farmer"]["yearForSaveGame"]), "year": int(save_info["Farmer"]["yearForSaveGame"]),
"season": int(save_info["Farmer"]["seasonForSaveGame"]), "season": int(save_info["Farmer"]["seasonForSaveGame"]),
"day": int(save_info["Farmer"]["dayOfMonthForSaveGame"]) "day": int(save_info["Farmer"]["dayOfMonthForSaveGame"]),
"game_version": save_info["Farmer"]["gameVersion"]
}, },
"files": { "file": {
"save": { "name": save_data_filename,
"name": save_data_filename, "uuid": zipped[0],
"uuid": save_data_file_id[0], "path": zipped[1]
"path": save_data_file_id[1]
},
"saveinfo": {
"name": "SaveGameInfo",
"uuid": save_info_file_id[0],
"path": save_info_file_id[1]
}
} }
} }
@@ -201,7 +211,7 @@ async def saves_post(device: str, files: List[UploadFile], apikey: APIKey = Depe
del save_info, save_data del save_info, save_data
del index["user"] del index["user"]
del index["files"] del index["file"]
del index["_id"] del index["_id"]
col_devices.find_one_and_update({"user": user, "name": device}, {"$set": {"last_save": save_date}}) col_devices.find_one_and_update({"user": user, "name": device}, {"$set": {"last_save": save_date}})

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

@@ -8,9 +8,14 @@ class StardewSaveData(BaseModel):
year: int year: int
season: int season: int
day: int day: int
game_version: str
class StardewSave(BaseModel): class StardewSave(BaseModel):
id: int id: int
device: str device: str
date: int date: int
data: StardewSaveData data: StardewSaveData
class StardewSaveBrief(BaseModel):
id: int
farmer: str

View File

@@ -1,4 +1,3 @@
from os import path
from typing import Union from typing import Union
from fastapi import FastAPI, Security, HTTPException from fastapi import FastAPI, Security, HTTPException
from starlette.status import HTTP_401_UNAUTHORIZED, HTTP_403_FORBIDDEN from starlette.status import HTTP_401_UNAUTHORIZED, HTTP_403_FORBIDDEN
@@ -6,7 +5,7 @@ from fastapi.security import APIKeyQuery, APIKeyHeader, APIKeyCookie
from fastapi.openapi.models import APIKey from fastapi.openapi.models import APIKey
from fastapi.openapi.docs import get_swagger_ui_html, get_redoc_html from fastapi.openapi.docs import get_swagger_ui_html, get_redoc_html
from starlette.status import HTTP_401_UNAUTHORIZED from starlette.status import HTTP_401_UNAUTHORIZED
from modules.utils import configGet, jsonLoad from modules.utils import configGet
from modules.security import passEncode from modules.security import passEncode
from modules.database import col_apikeys, col_expired from modules.database import col_apikeys, col_expired
@@ -14,33 +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)
def get_all_api_keys() -> list:
return jsonLoad(path.join(configGet("data", "locations"), "api_keys.json"))
def get_all_expired_keys() -> list:
return jsonLoad(path.join(configGet("data", "locations"), "expired_keys.json"))
# def check_project_key(project: str, apikey: APIKey) -> bool:
# keys = jsonLoad(path.join(configGet("data", "locations"), "api_keys.json"))
# if apikey in keys:
# if keys[apikey] != []:
# if project in keys[apikey]:
# return True
# else:
# return False
# else:
# return False
# else:
# return 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):
@@ -50,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"))
@@ -62,6 +37,7 @@ def user_by_key(apikey: Union[str, APIKey]) -> Union[str, None]:
db_key = col_apikeys.find_one({"hash": passEncode(apikey)}) db_key = col_apikeys.find_one({"hash": passEncode(apikey)})
return db_key["user"] if db_key is not None else None return db_key["user"] if db_key is not None else None
@app.get("/docs", include_in_schema=False) @app.get("/docs", include_in_schema=False)
async def custom_swagger_ui_html(): async def custom_swagger_ui_html():
return get_swagger_ui_html( return get_swagger_ui_html(

39
modules/mailer.py Normal file
View File

@@ -0,0 +1,39 @@
from smtplib import SMTP, SMTP_SSL
from traceback import print_exc
from ssl import create_default_context
from modules.utils import configGet, logWrite
try:
if configGet("use_ssl", "mailer", "smtp") is True:
mail_sender = SMTP_SSL(
configGet("host", "mailer", "smtp"),
configGet("port", "mailer", "smtp"),
)
logWrite(f"Initialized SMTP SSL connection")
elif configGet("use_tls", "mailer", "smtp") is True:
mail_sender = SMTP(
configGet("host", "mailer", "smtp"),
configGet("port", "mailer", "smtp"),
)
mail_sender.starttls(context=create_default_context())
mail_sender.ehlo()
logWrite(f"Initialized SMTP TLS connection")
else:
mail_sender = SMTP(
configGet("host", "mailer", "smtp"),
configGet("port", "mailer", "smtp")
)
mail_sender.ehlo()
logWrite(f"Initialized SMTP connection")
except Exception as exp:
logWrite(f"Could not initialize SMTP connection to: {exp}")
print_exc()
try:
mail_sender.login(
configGet("login", "mailer", "smtp"),
configGet("password", "mailer", "smtp")
)
logWrite(f"Successfully initialized mailer")
except Exception as exp:
logWrite(f"Could not login into provided SMTP account due to: {exp}")

View File

@@ -1,6 +1,7 @@
from os import makedirs, path from os import makedirs, path
from typing import Any, Tuple, Union from typing import Any, Tuple, Union
from uuid import uuid4 from uuid import uuid4
from zipfile import ZIP_DEFLATED, ZipFile
from ujson import loads, dumps, JSONDecodeError from ujson import loads, dumps, JSONDecodeError
from traceback import print_exc from traceback import print_exc
@@ -62,17 +63,24 @@ def configGet(key: str, *args: str) -> Any:
this_key = this_key[dict_key] this_key = this_key[dict_key]
return this_key[key] return this_key[key]
def saveFile(filebytes: bytes) -> Tuple[str, str]: def zip_saves(save_filename: str, save_bytes: bytes, saveinfo_bytes: bytes) -> Tuple[str, str]:
"""Save some bytedata into random file and return its ID """Save files of the SV save into archive and return uuid and path
### Args: ### Args:
* filebytes (`bytes`): Bytes to write into file * save_filename (`str`): Filename of the save file
vsave_bytes (`bytes`): Bytes of the save file
* saveinfo_bytes (`bytes`): Bytes of the save info file
### Returns: ### Returns:
* `Tuple[str, str]`: Tuple where first item is an ID and the second is an absolute path to file * `Tuple[str, str]`: First element is an UUID and the second is a filepath
""" """
makedirs(path.join(configGet("data", "locations"), "files"), exist_ok=True)
filename = str(uuid4()) save_uuid = str(uuid4())
with open(path.join(configGet("data", "locations"), "files", filename), "wb") as file:
file.write(filebytes) makedirs(path.join(configGet("data", "locations"), "files", save_uuid))
return filename, path.join(configGet("data", "locations"), "files", filename)
with ZipFile(path.join(configGet("data", "locations"), "files", save_uuid, save_filename+".svsave"), 'w', ZIP_DEFLATED, compresslevel=configGet("compression")) as ziph:
ziph.writestr("SaveGameInfo", saveinfo_bytes)
ziph.writestr(save_filename, save_bytes)
return save_uuid, path.join(configGet("data", "locations"), "files", save_uuid, save_filename+".svsave")

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

@@ -11,6 +11,8 @@ 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("-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()
@@ -18,8 +20,15 @@ args = parser.parse_args()
username = input("Enter username: ") if args.username is None else args.username username = input("Enter username: ") if args.username is None else args.username
if args.local is False:
email = input("Enter email: ") if args.email is None else args.email
if email.strip() == "":
email = None
else:
email = None
new_key = str(uuid4()) new_key = str(uuid4())
col_apikeys.insert_one({"user": username, "hash": passEncode(new_key)}) col_apikeys.insert_one({"user": username, "email": email, "hash": passEncode(new_key)})
if args.json is True and args.username is not None: if args.json is True and args.username is not None:
print(dumps({"apikey": new_key})) print(dumps({"apikey": new_key}))

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"]
} }

View File

@@ -13,15 +13,10 @@
"data.year", "data.year",
"data.season", "data.season",
"data.day", "data.day",
"files", "file",
"files.save", "file.name",
"files.save.name", "file.uuid",
"files.save.uuid", "file.path"
"files.save.path",
"files.saveinfo",
"files.saveinfo.name",
"files.saveinfo.uuid",
"files.saveinfo.path"
], ],
"properties": { "properties": {
"id": { "id": {
@@ -60,31 +55,16 @@
"data.day": { "data.day": {
"bsonType": "int" "bsonType": "int"
}, },
"files": { "file": {
"bsonType": "object" "bsonType": "object"
}, },
"files.save": { "file.name": {
"bsonType": "object"
},
"files.save.name": {
"bsonType": "string" "bsonType": "string"
}, },
"files.save.uuid": { "file.uuid": {
"bsonType": "string" "bsonType": "string"
}, },
"files.save.path": { "file.path": {
"bsonType": "string"
},
"files.saveinfo": {
"bsonType": "object"
},
"files.saveinfo.name": {
"bsonType": "string"
},
"files.saveinfo.uuid": {
"bsonType": "string"
},
"files.saveinfo.path": {
"bsonType": "string" "bsonType": "string"
} }
} }