2023-01-25 16:59:54 +02:00
from datetime import datetime , timezone
2023-01-18 15:25:22 +02:00
from urllib . parse import quote_plus
2023-01-19 14:47:18 +02:00
from os import remove
2023-01-21 19:02:36 +02:00
from typing import Dict , List , Literal , Union
2023-01-16 16:23:19 +02:00
from xmltodict import parse
2023-01-23 17:39:11 +02:00
from pymongo import DESCENDING
2023-01-23 11:36:28 +02:00
from models . saves import StardewSave , StardewSaveBrief
2023-01-18 15:25:22 +02:00
from modules . app import app , get_api_key , user_by_key
from modules . database import col_devices , col_saves
2023-01-16 16:23:19 +02:00
from fastapi import HTTPException , Depends , UploadFile
from fastapi . responses import UJSONResponse , FileResponse , Response
from fastapi . openapi . models import APIKey
2023-01-19 15:11:49 +02:00
from starlette . status import HTTP_204_NO_CONTENT , HTTP_403_FORBIDDEN , HTTP_404_NOT_FOUND , HTTP_406_NOT_ACCEPTABLE
2023-01-16 16:23:19 +02:00
2023-01-19 15:11:49 +02:00
from modules . utils import configGet , zip_saves
2023-01-16 16:23:19 +02:00
2023-01-23 11:36:28 +02:00
@app.get ( " /saves " , response_class = UJSONResponse , response_model = Union [ List [ Dict [ str , StardewSave ] ] , List [ StardewSaveBrief ] ] , description = " Get all available game saves " )
2023-01-22 12:31:00 +02:00
async def saves_get ( device : Union [ str , None ] = None , version : Union [ str , None ] = None , only_ids : bool = False , apikey : APIKey = Depends ( get_api_key ) ) :
2023-01-18 15:25:22 +02:00
2023-01-18 15:27:58 +02:00
user = user_by_key ( apikey )
2023-01-19 14:58:18 +02:00
query = { " user " : user }
if device is not None :
query [ " device " ] = device
if version is not None :
query [ " data.game_version " ] = version
2023-01-23 17:39:11 +02:00
saves_entries = list ( col_saves . find ( query ) . sort ( " date " , DESCENDING ) )
2023-01-18 15:25:22 +02:00
if len ( saves_entries ) == 0 :
2023-01-17 11:54:31 +02:00
raise HTTPException ( HTTP_404_NOT_FOUND , detail = " Could not find any saves. " )
2023-01-16 16:23:19 +02:00
2023-01-18 15:25:22 +02:00
output = [ ]
2023-01-23 17:39:11 +02:00
added = [ ]
2023-01-18 15:25:22 +02:00
2023-01-23 17:39:11 +02:00
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 )
2023-01-18 15:25:22 +02:00
return UJSONResponse ( output )
2023-01-16 16:23:19 +02:00
@app.get ( " /saves/ {id} " , response_class = UJSONResponse , response_model = List [ StardewSave ] , description = " Get game saves by name " )
2023-01-19 14:58:18 +02:00
async def saves_get_by_id ( id : int , device : Union [ str , None ] = None , version : Union [ str , None ] = None , apikey : APIKey = Depends ( get_api_key ) ) :
query = { " id " : id }
if device is not None :
query [ " device " ] = device
2023-01-18 15:25:22 +02:00
2023-01-19 14:58:18 +02:00
if version is not None :
query [ " data.game_version " ] = version
2023-01-23 17:39:11 +02:00
saves_entries = list ( col_saves . find ( query ) . sort ( " date " , DESCENDING ) )
2023-01-18 15:25:22 +02:00
if len ( saves_entries ) == 0 :
2023-01-17 11:54:31 +02:00
raise HTTPException ( HTTP_404_NOT_FOUND , detail = " Could not find save with such id. " )
2023-01-16 16:23:19 +02:00
2023-01-18 15:25:22 +02:00
output = [ ]
2023-01-16 16:23:19 +02:00
2023-01-18 15:25:22 +02:00
for entry in saves_entries :
out_entry = entry
del out_entry [ " _id " ]
2023-01-19 14:35:27 +02:00
del out_entry [ " file " ]
2023-01-18 15:25:22 +02:00
del out_entry [ " user " ]
output . append ( out_entry )
return UJSONResponse ( output )
2023-01-21 19:02:36 +02:00
@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 ) ) :
2023-01-19 14:58:18 +02:00
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 )
2023-01-18 15:25:22 +02:00
if saves_entry is not None :
del saves_entry [ " _id " ]
2023-01-19 14:35:27 +02:00
del saves_entry [ " file " ]
2023-01-18 15:25:22 +02:00
return UJSONResponse ( saves_entry )
2023-01-17 11:54:31 +02:00
else :
raise HTTPException ( HTTP_404_NOT_FOUND , detail = " Could not find save with such id. " )
2023-01-21 19:02:36 +02:00
@app.delete ( " /saves/ {id} " , description = " Delete game saves by id " )
2023-01-18 15:25:22 +02:00
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 :
2023-01-19 14:35:27 +02:00
remove ( entry [ " file " ] [ " path " ] )
2023-01-18 15:25:22 +02:00
col_saves . delete_many ( { " user " : user , " id " : id } )
2023-01-17 11:54:31 +02:00
return Response ( status_code = HTTP_204_NO_CONTENT )
else :
raise HTTPException ( HTTP_404_NOT_FOUND , detail = " Could not find save with such id. " )
2023-01-21 19:02:36 +02:00
@app.delete ( " /saves/ {id} / {save_date} " , response_class = UJSONResponse , description = " Delete game saves by id and upload date " )
2023-01-18 15:25:22 +02:00
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 :
2023-01-19 14:35:27 +02:00
remove ( saves_entry [ " file " ] [ " path " ] )
2023-01-18 15:25:22 +02:00
return Response ( status_code = HTTP_204_NO_CONTENT )
2023-01-17 11:54:31 +02:00
else :
raise HTTPException ( HTTP_404_NOT_FOUND , detail = " Could not find save with such id. " )
2023-01-21 19:02:36 +02:00
@app.get ( " /saves/ {id} / {save_date} /download " , response_class = FileResponse , description = " Get game save as .svsave file by its id and upload date " )
2023-01-19 14:35:27 +02:00
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 } )
2023-01-18 15:25:22 +02:00
if saves_entry is not None : # type: ignore
2023-01-19 14:35:27 +02:00
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
2023-01-16 16:23:19 +02:00
else :
2023-01-17 11:54:31 +02:00
raise HTTPException ( HTTP_404_NOT_FOUND , detail = " Could not find save with such id. " )
2023-01-16 16:23:19 +02:00
@app.post ( " /saves " , response_class = UJSONResponse , response_model = StardewSave , description = " Upload new save " )
async def saves_post ( device : str , files : List [ UploadFile ] , apikey : APIKey = Depends ( get_api_key ) ) :
2023-01-18 15:25:22 +02:00
user = user_by_key ( apikey )
2023-01-16 16:32:52 +02:00
error_return = HTTPException ( HTTP_406_NOT_ACCEPTABLE , detail = " You must provide two files: save file and SaveGameInfo for that save " )
2023-01-16 16:23:19 +02:00
2023-01-19 15:13:14 +02:00
if ( configGet ( " saves " , " limits " ) != - 1 ) and ( col_saves . count_documents ( { " user " : user } ) > = configGet ( " saves " , " limits " ) ) :
2023-01-19 15:11:49 +02:00
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
)
2023-01-16 16:23:19 +02:00
if len ( files ) != 2 :
return error_return
2023-01-16 16:32:52 +02:00
if " SaveGameInfo " not in [ file . filename for file in files ] :
2023-01-16 16:23:19 +02:00
return error_return
2023-01-18 15:25:22 +02:00
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. " )
2023-01-19 14:35:27 +02:00
save_info = save_data = save_info_file = save_data_filename = save_data_file = None
2023-01-16 16:23:19 +02:00
for file in files :
2023-01-16 16:32:52 +02:00
if file . filename == " SaveGameInfo " :
2023-01-16 16:23:19 +02:00
save_info_file = await file . read ( )
save_info = parse ( save_info_file . decode ( " utf-8 " ) )
if " Farmer " not in save_info :
return error_return
else :
2023-01-18 15:25:22 +02:00
save_data_filename = file . filename
2023-01-16 16:23:19 +02:00
save_data_file = await file . read ( )
save_data = parse ( save_data_file . decode ( " utf-8 " ) )
if " SaveGame " not in save_data :
return error_return
2023-01-19 14:35:27 +02:00
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 :
2023-01-16 16:23:19 +02:00
return error_return
2023-01-19 14:35:27 +02:00
zipped = zip_saves ( save_data_filename , save_data_file , save_info_file )
2023-01-25 16:59:54 +02:00
save_date = int ( datetime . now ( tz = timezone . utc ) . timestamp ( ) )
2023-01-16 16:23:19 +02:00
index = {
" id " : int ( save_data [ " SaveGame " ] [ " uniqueIDForThisGame " ] ) ,
2023-01-18 15:25:22 +02:00
" user " : user ,
2023-01-16 16:23:19 +02:00
" device " : device ,
2023-01-18 15:25:22 +02:00
" date " : save_date ,
" data " : {
" farmer " : save_info [ " Farmer " ] [ " name " ] ,
" money " : int ( save_info [ " Farmer " ] [ " money " ] ) ,
" played " : int ( save_info [ " Farmer " ] [ " millisecondsPlayed " ] ) ,
" save_time " : int ( save_info [ " Farmer " ] [ " saveTime " ] ) ,
" year " : int ( save_info [ " Farmer " ] [ " yearForSaveGame " ] ) ,
" season " : int ( save_info [ " Farmer " ] [ " seasonForSaveGame " ] ) ,
2023-01-19 14:47:18 +02:00
" day " : int ( save_info [ " Farmer " ] [ " dayOfMonthForSaveGame " ] ) ,
" game_version " : save_info [ " Farmer " ] [ " gameVersion " ]
2023-01-18 15:25:22 +02:00
} ,
2023-01-19 14:35:27 +02:00
" file " : {
" name " : save_data_filename ,
" uuid " : zipped [ 0 ] ,
" path " : zipped [ 1 ]
2023-01-18 15:25:22 +02:00
}
2023-01-16 16:23:19 +02:00
}
2023-01-18 15:25:22 +02:00
col_saves . insert_one ( index )
del save_info , save_data
del index [ " user " ]
2023-01-19 14:35:27 +02:00
del index [ " file " ]
2023-01-18 15:25:22 +02:00
del index [ " _id " ]
2023-01-16 16:23:19 +02:00
2023-01-18 15:25:22 +02:00
col_devices . find_one_and_update ( { " user " : user , " name " : device } , { " $set " : { " last_save " : save_date } } )
2023-01-16 16:23:19 +02:00
return UJSONResponse ( index )