Compare commits
20 Commits
e9e9e3784a
...
master
Author | SHA1 | Date | |
---|---|---|---|
c0d608b63f | |||
dd7031ff19 | |||
1294d1a037 | |||
546600a29e | |||
|
30f11a7c83 | ||
|
14d1ba9fa7 | ||
|
ce768d895d | ||
daa3b0ca73 | |||
673c986ff9 | |||
453c3e95dd | |||
aff3f76fc1 | |||
1ce8c0d712 | |||
5f8dff42c3 | |||
02552dce5a | |||
84be0c7154 | |||
08d060e160 | |||
aa8be6006c | |||
3c245d8671 | |||
335a497991 | |||
1beca94cc0 |
25
config.json
25
config.json
@@ -9,6 +9,11 @@
|
||||
"locations": {
|
||||
"data": "data"
|
||||
},
|
||||
"compression": 5,
|
||||
"limits": {
|
||||
"saves": -1,
|
||||
"devices": -1
|
||||
},
|
||||
"messages": {
|
||||
"key_expired": "API key expired",
|
||||
"key_invalid": "Invalid API key",
|
||||
@@ -19,5 +24,25 @@
|
||||
"user_already_exists": "User with this username already exists.",
|
||||
"email_confirmed": "Email confirmed. You can now log in.",
|
||||
"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}"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@@ -3,7 +3,9 @@ from modules.database import col_devices, col_saves
|
||||
from fastapi import HTTPException, Depends
|
||||
from fastapi.responses import UJSONResponse, Response
|
||||
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")
|
||||
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})
|
||||
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.")
|
||||
@@ -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.")
|
||||
|
||||
@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)
|
||||
|
||||
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:
|
||||
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)
|
||||
|
||||
@@ -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:
|
||||
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)
|
@@ -1,74 +1,68 @@
|
||||
from datetime import datetime
|
||||
from io import BytesIO
|
||||
from datetime import datetime, timezone
|
||||
from urllib.parse import quote_plus
|
||||
from os import path, remove
|
||||
from typing import Dict, List, Union
|
||||
from zipfile import ZipFile
|
||||
from os import remove
|
||||
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.utils import saveFile
|
||||
from modules.database import col_devices, col_saves
|
||||
from fastapi import HTTPException, Depends, UploadFile
|
||||
from fastapi.responses import UJSONResponse, FileResponse, Response
|
||||
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()
|
||||
zf = ZipFile(s, "w")
|
||||
query = {"user": user}
|
||||
|
||||
for fpath in filenames:
|
||||
if device is not None:
|
||||
query["device"] = device
|
||||
|
||||
# Calculate path for file in zip
|
||||
fdir, fname = path.split(fpath)
|
||||
if version is not None:
|
||||
query["data.game_version"] = version
|
||||
|
||||
# Add file, at correct path
|
||||
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)}))
|
||||
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["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)
|
||||
|
||||
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, 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:
|
||||
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:
|
||||
out_entry = entry
|
||||
del out_entry["_id"]
|
||||
del out_entry["files"]
|
||||
del out_entry["file"]
|
||||
del out_entry["user"]
|
||||
output.append(out_entry)
|
||||
|
||||
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, apikey: APIKey = Depends(get_api_key)):
|
||||
saves_entry = col_saves.find_one({"user": user_by_key(apikey), "id": id, "date": save_date})
|
||||
@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}
|
||||
|
||||
if device is not None:
|
||||
query["device"] = device
|
||||
|
||||
saves_entry = col_saves.find_one(query)
|
||||
|
||||
if saves_entry is not None:
|
||||
del saves_entry["_id"]
|
||||
del saves_entry["files"]
|
||||
del saves_entry["file"]
|
||||
return UJSONResponse(saves_entry)
|
||||
else:
|
||||
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:
|
||||
saves_entries = list(col_saves.find({"user": user, "id": id}))
|
||||
for entry in saves_entries:
|
||||
remove(entry["files"]["save"]["path"])
|
||||
remove(entry["files"]["saveinfo"]["path"])
|
||||
remove(entry["file"]["path"])
|
||||
col_saves.delete_many({"user": user, "id": id})
|
||||
return Response(status_code=HTTP_204_NO_CONTENT)
|
||||
else:
|
||||
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:
|
||||
remove(saves_entry["files"]["save"]["path"])
|
||||
remove(saves_entry["files"]["saveinfo"]["path"])
|
||||
remove(saves_entry["file"]["path"])
|
||||
return Response(status_code=HTTP_204_NO_CONTENT)
|
||||
else:
|
||||
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")
|
||||
async def saves_download(id: int, save_date: int, apikey: APIKey = Depends(get_api_key)):
|
||||
saves_entry = col_saves.find_one({"user": user_by_key(apikey), "id": id, "date": 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
|
||||
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:
|
||||
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")
|
||||
|
||||
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:
|
||||
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:
|
||||
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:
|
||||
if file.filename == "SaveGameInfo":
|
||||
save_info_file = await file.read()
|
||||
save_info_file_id = saveFile(save_info_file)
|
||||
save_info = parse(save_info_file.decode("utf-8"))
|
||||
if "Farmer" not in save_info:
|
||||
return error_return
|
||||
else:
|
||||
save_data_filename = file.filename
|
||||
save_data_file = await file.read()
|
||||
save_data_file_id = saveFile(save_data_file)
|
||||
save_data = parse(save_data_file.decode("utf-8"))
|
||||
if "SaveGame" not in save_data:
|
||||
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
|
||||
|
||||
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 = {
|
||||
"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"]),
|
||||
"year": int(save_info["Farmer"]["yearForSaveGame"]),
|
||||
"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": {
|
||||
"save": {
|
||||
"file": {
|
||||
"name": save_data_filename,
|
||||
"uuid": save_data_file_id[0],
|
||||
"path": save_data_file_id[1]
|
||||
},
|
||||
"saveinfo": {
|
||||
"name": "SaveGameInfo",
|
||||
"uuid": save_info_file_id[0],
|
||||
"path": save_info_file_id[1]
|
||||
}
|
||||
"uuid": zipped[0],
|
||||
"path": zipped[1]
|
||||
}
|
||||
}
|
||||
|
||||
@@ -201,7 +211,7 @@ async def saves_post(device: str, files: List[UploadFile], apikey: APIKey = Depe
|
||||
del save_info, save_data
|
||||
|
||||
del index["user"]
|
||||
del index["files"]
|
||||
del index["file"]
|
||||
del index["_id"]
|
||||
|
||||
col_devices.find_one_and_update({"user": user, "name": device}, {"$set": {"last_save": save_date}})
|
||||
|
@@ -4,4 +4,5 @@ class Device(BaseModel):
|
||||
user: str
|
||||
name: str
|
||||
os: str
|
||||
client: str
|
||||
last_save: int
|
@@ -8,9 +8,14 @@ class StardewSaveData(BaseModel):
|
||||
year: int
|
||||
season: int
|
||||
day: int
|
||||
game_version: str
|
||||
|
||||
class StardewSave(BaseModel):
|
||||
id: int
|
||||
device: str
|
||||
date: int
|
||||
data: StardewSaveData
|
||||
|
||||
class StardewSaveBrief(BaseModel):
|
||||
id: int
|
||||
farmer: str
|
@@ -1,4 +1,3 @@
|
||||
from os import path
|
||||
from typing import Union
|
||||
from fastapi import FastAPI, Security, HTTPException
|
||||
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.docs import get_swagger_ui_html, get_redoc_html
|
||||
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.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_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(
|
||||
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):
|
||||
@@ -50,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"))
|
||||
@@ -62,6 +37,7 @@ def user_by_key(apikey: Union[str, APIKey]) -> Union[str, None]:
|
||||
db_key = col_apikeys.find_one({"hash": passEncode(apikey)})
|
||||
return db_key["user"] if db_key is not None else None
|
||||
|
||||
|
||||
@app.get("/docs", include_in_schema=False)
|
||||
async def custom_swagger_ui_html():
|
||||
return get_swagger_ui_html(
|
||||
|
39
modules/mailer.py
Normal file
39
modules/mailer.py
Normal 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}")
|
@@ -1,6 +1,7 @@
|
||||
from os import makedirs, path
|
||||
from typing import Any, Tuple, Union
|
||||
from uuid import uuid4
|
||||
from zipfile import ZIP_DEFLATED, ZipFile
|
||||
from ujson import loads, dumps, JSONDecodeError
|
||||
from traceback import print_exc
|
||||
|
||||
@@ -62,17 +63,24 @@ def configGet(key: str, *args: str) -> Any:
|
||||
this_key = this_key[dict_key]
|
||||
return this_key[key]
|
||||
|
||||
def saveFile(filebytes: bytes) -> Tuple[str, str]:
|
||||
"""Save some bytedata into random file and return its ID
|
||||
def zip_saves(save_filename: str, save_bytes: bytes, saveinfo_bytes: bytes) -> Tuple[str, str]:
|
||||
"""Save files of the SV save into archive and return uuid and path
|
||||
|
||||
### 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:
|
||||
* `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())
|
||||
with open(path.join(configGet("data", "locations"), "files", filename), "wb") as file:
|
||||
file.write(filebytes)
|
||||
return filename, path.join(configGet("data", "locations"), "files", filename)
|
||||
|
||||
save_uuid = str(uuid4())
|
||||
|
||||
makedirs(path.join(configGet("data", "locations"), "files", save_uuid))
|
||||
|
||||
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")
|
@@ -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():
|
||||
|
11
sync_gen.py
11
sync_gen.py
@@ -11,6 +11,8 @@ 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_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()
|
||||
@@ -18,8 +20,15 @@ args = parser.parse_args()
|
||||
|
||||
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())
|
||||
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:
|
||||
print(dumps({"apikey": new_key}))
|
||||
|
@@ -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"]
|
||||
}
|
||||
|
@@ -13,15 +13,10 @@
|
||||
"data.year",
|
||||
"data.season",
|
||||
"data.day",
|
||||
"files",
|
||||
"files.save",
|
||||
"files.save.name",
|
||||
"files.save.uuid",
|
||||
"files.save.path",
|
||||
"files.saveinfo",
|
||||
"files.saveinfo.name",
|
||||
"files.saveinfo.uuid",
|
||||
"files.saveinfo.path"
|
||||
"file",
|
||||
"file.name",
|
||||
"file.uuid",
|
||||
"file.path"
|
||||
],
|
||||
"properties": {
|
||||
"id": {
|
||||
@@ -60,31 +55,16 @@
|
||||
"data.day": {
|
||||
"bsonType": "int"
|
||||
},
|
||||
"files": {
|
||||
"file": {
|
||||
"bsonType": "object"
|
||||
},
|
||||
"files.save": {
|
||||
"bsonType": "object"
|
||||
},
|
||||
"files.save.name": {
|
||||
"file.name": {
|
||||
"bsonType": "string"
|
||||
},
|
||||
"files.save.uuid": {
|
||||
"file.uuid": {
|
||||
"bsonType": "string"
|
||||
},
|
||||
"files.save.path": {
|
||||
"bsonType": "string"
|
||||
},
|
||||
"files.saveinfo": {
|
||||
"bsonType": "object"
|
||||
},
|
||||
"files.saveinfo.name": {
|
||||
"bsonType": "string"
|
||||
},
|
||||
"files.saveinfo.uuid": {
|
||||
"bsonType": "string"
|
||||
},
|
||||
"files.saveinfo.path": {
|
||||
"file.path": {
|
||||
"bsonType": "string"
|
||||
}
|
||||
}
|
||||
|
Reference in New Issue
Block a user