forked from FOSS/Steam-Panel
280 lines
11 KiB
Python
280 lines
11 KiB
Python
import json
|
|
import io
|
|
import zipfile
|
|
from pathlib import Path
|
|
|
|
from fastapi import APIRouter, HTTPException, UploadFile, File
|
|
from fastapi.responses import StreamingResponse
|
|
from loguru import logger
|
|
from pydantic import BaseModel
|
|
|
|
from app.config import settings
|
|
from app.database import get_db
|
|
from app.models import MafileData
|
|
|
|
router = APIRouter(prefix="/api/mafiles", tags=["mafile_tools"])
|
|
|
|
|
|
@router.get("")
|
|
async def list_mafiles():
|
|
"""List all stored mafiles."""
|
|
mafiles_dir = settings.mafiles_dir
|
|
if not mafiles_dir.exists():
|
|
return []
|
|
|
|
result = []
|
|
for f in sorted(mafiles_dir.glob("*.mafile")):
|
|
try:
|
|
data = json.loads(f.read_text(encoding="utf-8"))
|
|
result.append({
|
|
"filename": f.name,
|
|
"account_name": data.get("account_name", ""),
|
|
"steam_id": data.get("Session", {}).get("SteamID", ""),
|
|
"has_shared_secret": bool(data.get("shared_secret")),
|
|
"has_identity_secret": bool(data.get("identity_secret")),
|
|
"fully_enrolled": data.get("fully_enrolled", False),
|
|
})
|
|
except (json.JSONDecodeError, OSError):
|
|
result.append({"filename": f.name, "error": "Failed to parse"})
|
|
return result
|
|
|
|
|
|
@router.post("/upload")
|
|
async def upload_mafiles(files: list[UploadFile] = File(...)):
|
|
"""Upload multiple .mafile files."""
|
|
settings.mafiles_dir.mkdir(parents=True, exist_ok=True)
|
|
uploaded = 0
|
|
errors: list[str] = []
|
|
|
|
db = await get_db()
|
|
|
|
for file in files:
|
|
content = (await file.read()).decode("utf-8", errors="ignore")
|
|
try:
|
|
mafile = MafileData.model_validate_json(content)
|
|
steam_id = mafile.Session.SteamID or mafile.account_name or file.filename
|
|
safe_name = Path(str(steam_id)).name
|
|
dest = settings.mafiles_dir / f"{safe_name}.mafile"
|
|
dest.write_text(content, encoding="utf-8")
|
|
uploaded += 1
|
|
|
|
if mafile.account_name:
|
|
mafile_steam_id = str(mafile.Session.SteamID) if mafile.Session.SteamID else None
|
|
await db.execute(
|
|
"""UPDATE accounts
|
|
SET mafile_path = ?, shared_secret = ?, identity_secret = ?,
|
|
steam_id = COALESCE(steam_id, ?)
|
|
WHERE login = ?""",
|
|
(
|
|
str(dest),
|
|
mafile.shared_secret or "",
|
|
mafile.identity_secret or "",
|
|
mafile_steam_id,
|
|
mafile.account_name,
|
|
),
|
|
)
|
|
await db.commit()
|
|
except Exception as exc:
|
|
errors.append(f"{file.filename}: {exc}")
|
|
|
|
logger.info(f"Uploaded {uploaded} mafiles, {len(errors)} errors")
|
|
return {"uploaded": uploaded, "errors": errors}
|
|
|
|
|
|
@router.get("/{filename}")
|
|
async def get_mafile(filename: str):
|
|
"""Get parsed mafile data by filename."""
|
|
safe_name = Path(filename).name
|
|
mafile_path = settings.mafiles_dir / safe_name
|
|
if not mafile_path.exists() or not mafile_path.suffix == ".mafile":
|
|
raise HTTPException(status_code=404, detail="Mafile not found")
|
|
|
|
content = mafile_path.read_text(encoding="utf-8")
|
|
try:
|
|
return json.loads(content)
|
|
except json.JSONDecodeError:
|
|
raise HTTPException(status_code=500, detail="Failed to parse mafile")
|
|
|
|
|
|
@router.delete("/{filename}", status_code=204)
|
|
async def delete_mafile(filename: str):
|
|
safe_name = Path(filename).name
|
|
mafile_path = settings.mafiles_dir / safe_name
|
|
if not mafile_path.exists():
|
|
raise HTTPException(status_code=404, detail="Mafile not found")
|
|
mafile_path.unlink()
|
|
|
|
|
|
@router.get("/export/all")
|
|
async def export_all_secrets():
|
|
"""Export account_name:shared_secret:identity_secret for all mafiles."""
|
|
mafiles_dir = settings.mafiles_dir
|
|
if not mafiles_dir.exists():
|
|
return []
|
|
|
|
result = []
|
|
for f in sorted(mafiles_dir.glob("*.mafile")):
|
|
try:
|
|
data = json.loads(f.read_text(encoding="utf-8"))
|
|
result.append({
|
|
"account_name": data.get("account_name", ""),
|
|
"shared_secret": data.get("shared_secret", ""),
|
|
"identity_secret": data.get("identity_secret", ""),
|
|
"steam_id": data.get("Session", {}).get("SteamID", ""),
|
|
})
|
|
except (json.JSONDecodeError, OSError):
|
|
pass
|
|
return result
|
|
|
|
|
|
class ExportRequest(BaseModel):
|
|
fields: list[str] = []
|
|
session_fields: list[str] = []
|
|
format: str = "flat_mafiles" # flat_mafiles | per_account_folder | single_file
|
|
account_ids: list[int] = []
|
|
# Naming variables: {username} and {steamid} are replaced at export time
|
|
folder_name_template: str = "{username}"
|
|
mafile_name_template: str = "{steamid}.mafile"
|
|
txt_name_template: str = "{username}.txt"
|
|
include_txt_per_folder: bool = False
|
|
include_global_txt: bool = False
|
|
skip_folders: bool = False
|
|
# Format for .txt lines: login:password:email:email_password
|
|
txt_format: str = "{login}:{password}:{email}:{email_password}"
|
|
|
|
|
|
def _filter_mafile(data: dict, fields: list[str], session_fields: list[str]) -> dict:
|
|
"""Keep only selected top-level + Session fields from mafile JSON."""
|
|
if not fields and not session_fields:
|
|
return data
|
|
|
|
filtered = {}
|
|
for key in fields:
|
|
if key in data and key != "Session":
|
|
filtered[key] = data[key]
|
|
|
|
if session_fields and "Session" in data:
|
|
filtered["Session"] = {
|
|
k: v for k, v in data["Session"].items() if k in session_fields
|
|
}
|
|
|
|
return filtered
|
|
|
|
|
|
@router.post("/export/zip")
|
|
async def export_mafiles_zip(body: ExportRequest):
|
|
"""Build a .zip with filtered mafile data, optionally limited to account IDs."""
|
|
if not body.account_ids:
|
|
raise HTTPException(status_code=400, detail="No account IDs provided for export")
|
|
|
|
mafiles_dir = settings.mafiles_dir
|
|
if not mafiles_dir.exists():
|
|
raise HTTPException(status_code=404, detail="No mafiles directory")
|
|
|
|
db = await get_db()
|
|
|
|
# Load account info for naming and .txt generation
|
|
account_map: dict[str, dict] = {} # login -> account row
|
|
if body.account_ids:
|
|
placeholders = ",".join("?" * len(body.account_ids))
|
|
cursor = await db.execute(
|
|
f"SELECT * FROM accounts WHERE id IN ({placeholders})",
|
|
body.account_ids,
|
|
)
|
|
else:
|
|
cursor = await db.execute("SELECT * FROM accounts")
|
|
for row in await cursor.fetchall():
|
|
account_map[row["login"]] = dict(row)
|
|
|
|
account_logins = set(account_map.keys()) if body.account_ids else set()
|
|
|
|
def _resolve_template(template: str, username: str, steam_id: str) -> str:
|
|
return template.replace("{username}", username).replace("{steamid}", steam_id)
|
|
|
|
def _resolve_txt_line(template: str, acc: dict, mafile_data: dict | None = None) -> str:
|
|
has_mafile_var = "{mafile}" in template
|
|
if has_mafile_var:
|
|
before, _, after = template.partition("{mafile}")
|
|
else:
|
|
before = template
|
|
|
|
line = (
|
|
before
|
|
.replace("{login}", acc.get("login") or "")
|
|
.replace("{password}", acc.get("password") or "")
|
|
.replace("{email}", acc.get("email") or "")
|
|
.replace("{email_password}", acc.get("email_password") or "")
|
|
.replace("{steam_id}", acc.get("steam_id") or "")
|
|
.replace("{proxy}", acc.get("proxy") or "")
|
|
)
|
|
line = line.rstrip(":")
|
|
if has_mafile_var and mafile_data is not None:
|
|
line += ":" + json.dumps(mafile_data, ensure_ascii=False, separators=(",", ":"))
|
|
return line
|
|
|
|
buf = io.BytesIO()
|
|
all_data: list[str] = []
|
|
global_txt_lines: list[str] = []
|
|
folder_txt_map: dict[str, list[str]] = {} # folder_name -> lines
|
|
|
|
with zipfile.ZipFile(buf, "w", zipfile.ZIP_DEFLATED) as zf:
|
|
for f in sorted(mafiles_dir.glob("*.mafile")):
|
|
try:
|
|
raw = json.loads(f.read_text(encoding="utf-8"))
|
|
except (json.JSONDecodeError, OSError):
|
|
continue
|
|
|
|
acct_name = raw.get("account_name", "")
|
|
steam_id = str(raw.get("Session", {}).get("SteamID", ""))
|
|
|
|
if account_logins:
|
|
if acct_name not in account_logins and steam_id not in {str(a) for a in body.account_ids}:
|
|
continue
|
|
|
|
filtered = _filter_mafile(raw, body.fields, body.session_fields)
|
|
|
|
# Find matching account record for txt
|
|
acc = account_map.get(acct_name, {})
|
|
|
|
if body.format == "single_file":
|
|
if acc:
|
|
all_data.append(_resolve_txt_line(body.txt_format, acc, filtered))
|
|
else:
|
|
all_data.append(json.dumps(filtered, ensure_ascii=False, separators=(",", ":")))
|
|
elif body.format == "per_account_folder":
|
|
folder_name = _resolve_template(body.folder_name_template, acct_name, steam_id)
|
|
mafile_name = _resolve_template(body.mafile_name_template, acct_name, steam_id)
|
|
|
|
if body.skip_folders:
|
|
zf.writestr(mafile_name, json.dumps(filtered, ensure_ascii=False, separators=(",", ":")))
|
|
else:
|
|
zf.writestr(f"{folder_name}/{mafile_name}", json.dumps(filtered, ensure_ascii=False, separators=(",", ":")))
|
|
|
|
if body.include_txt_per_folder and acc:
|
|
txt_name = _resolve_template(body.txt_name_template, acct_name, steam_id)
|
|
line = _resolve_txt_line(body.txt_format, acc, filtered)
|
|
if folder_name not in folder_txt_map:
|
|
folder_txt_map[folder_name] = []
|
|
folder_txt_map[folder_name].append(line)
|
|
zf.writestr(f"{folder_name}/{txt_name}", line + "\n")
|
|
else:
|
|
mafile_name = _resolve_template(body.mafile_name_template, acct_name, steam_id)
|
|
zf.writestr(mafile_name, json.dumps(filtered, ensure_ascii=False, separators=(",", ":")))
|
|
|
|
# Collect global txt line
|
|
if body.include_global_txt and acc:
|
|
global_txt_lines.append(_resolve_txt_line(body.txt_format, acc, filtered))
|
|
|
|
if body.format == "single_file" and all_data:
|
|
zf.writestr("accounts.txt", "\n".join(all_data) + "\n")
|
|
|
|
if body.include_global_txt and global_txt_lines:
|
|
zf.writestr("accounts.txt", "\n".join(global_txt_lines) + "\n")
|
|
|
|
buf.seek(0)
|
|
return StreamingResponse(
|
|
buf,
|
|
media_type="application/zip",
|
|
headers={"Content-Disposition": "attachment; filename=mafiles_export.zip"},
|
|
)
|