Files
idrs-api/app/db.py
T

253 lines
7.7 KiB
Python

import json
import logging
import os
import re
import urllib.error
import urllib.request
from pathlib import Path
from urllib.parse import urlparse
from app.models import (
ICON_DIR,
SessionLocal,
Dapp,
Token,
Version,
Wallet,
Base,
engine,
)
logger = logging.getLogger("idrs.seed")
BASE_DIR = Path(__file__).resolve().parent.parent
SEED_TOKEN = BASE_DIR / "tokenList.json"
SEED_VERSION = BASE_DIR / "versionAndroid.json"
SEED_DAPPS = BASE_DIR / "dapps.json"
PUBLIC_BASE_URL = os.environ.get("IDRS_PUBLIC_URL", "https://idrs.databisnis.id").rstrip("/")
IMAGE_TIMEOUT = 8
IMAGE_USER_AGENT = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0 Safari/537.36"
IMAGE_MAGIC = [
(b"\x89PNG\r\n\x1a\n", ".png"),
(b"\xff\xd8\xff", ".jpg"),
(b"GIF87a", ".gif"),
(b"GIF89a", ".gif"),
(b"RIFF", ".webp"),
(b"\x00\x00\x01\x00", ".ico"),
(b"BM", ".bmp"),
]
IMAGE_EXT = {
"image/png": ".png",
"image/jpeg": ".jpg",
"image/jpg": ".jpg",
"image/gif": ".gif",
"image/webp": ".webp",
"image/x-icon": ".ico",
"image/vnd.microsoft.icon": ".ico",
"image/svg+xml": ".svg",
"image/bmp": ".bmp",
}
def _ensure_migrations(engine):
from sqlalchemy import inspect
inspected = inspect(engine)
cols = {c["name"] for c in inspected.get_columns("dapps")} if "dapps" in inspected.get_table_names() else set()
if "is_live_override" not in cols:
from sqlalchemy import text
with engine.begin() as conn:
conn.execute(text("ALTER TABLE dapps ADD COLUMN is_live_override BOOLEAN NOT NULL DEFAULT 0"))
if "is_new_version" not in cols:
from sqlalchemy import text
with engine.begin() as conn:
conn.execute(text("ALTER TABLE dapps ADD COLUMN is_new_version BOOLEAN NOT NULL DEFAULT 0"))
def init_db():
Base.metadata.create_all(engine)
_ensure_migrations(engine)
seed()
def seed():
session = SessionLocal()
try:
if session.query(Token).count() == 0 and SEED_TOKEN.exists():
data = json.loads(SEED_TOKEN.read_text(encoding="utf-8"))
for t in data["tokenList"]:
session.add(
Token(
idfToken=int(t["idfToken"]),
position=int(t["position"]),
typeBlockchain=t["typeBlockchain"],
name=t["name"],
symbol=t["symbol"],
contractAddr=t["contractAddr"],
decimals=int(t["decimals"]),
iconUrl=t.get("iconUrl") or "",
filename=t.get("filename") or "",
is_live=t.get("is_live") in (1, "1", "true", True),
)
)
if session.query(Version).count() == 0 and SEED_VERSION.exists():
v = json.loads(SEED_VERSION.read_text(encoding="utf-8"))
session.add(
Version(
id=1,
idVersion=int(v["idVersion"]),
appName=v["appName"],
acceptableVersion=int(v["acceptableVersion"]),
latestVersion=int(v["latestVersion"]),
)
)
seed_dapps(session)
seed_wallets(session)
session.commit()
finally:
session.close()
SEED_WALLETS = ["susukudaliar", "vexessential"]
def _utc_now_iso() -> str:
from datetime import datetime, timezone
return datetime.now(timezone.utc).isoformat(timespec="seconds")
def seed_wallets(session):
if session.query(Wallet).count() != 0:
return
for account in SEED_WALLETS:
session.add(Wallet(account=account, created_at=_utc_now_iso()))
def _image_extension(content_type: str, url: str) -> str:
ext = IMAGE_EXT.get((content_type or "").split(";")[0].strip().lower())
if ext:
return ext
m = re.search(r"\.(png|jpe?g|gif|webp|ico|bmp|svg)$", url.lower())
return "." + m.group(1) if m else ".img"
def _mirror_image(url: str, seen: dict):
if url in seen:
return seen[url]
try:
req = urllib.request.Request(
url, headers={"User-Agent": IMAGE_USER_AGENT, "Accept": "image/*,*/*"}
)
with urllib.request.urlopen(req, timeout=IMAGE_TIMEOUT) as resp:
content_type = (resp.headers.get_content_type() or "").split(";")[0].strip().lower()
data = resp.read()
except Exception as e:
logger.warning("dapp image fetch failed %r for %s", e, url)
return None
if not data:
return None
if content_type == "application/octet-stream":
for magic, ext in IMAGE_MAGIC:
if data.startswith(magic):
content_type = next((k for k, v in IMAGE_EXT.items() if v == ext), content_type)
break
else:
content_type = ""
if not content_type.startswith("image/"):
logger.warning("dapp image %s not an image (%s)", url, content_type)
return None
base = re.sub(r"[^A-Za-z0-9._-]+", "_", Path(urlparse(url).path).name).strip("._") or "dapp"
stem = Path(base).stem[:80]
ext = _image_extension(content_type, url)
dapp_dir = ICON_DIR / "dapps"
dapp_dir.mkdir(parents=True, exist_ok=True)
filename = f"{stem}{ext}"
n = 1
while (dapp_dir / filename).exists():
filename = f"{stem}-{n}{ext}"
n += 1
(dapp_dir / filename).write_bytes(data)
seen[url] = f"{PUBLIC_BASE_URL}/img/dapps/{filename}"
return seen[url]
def seed_dapps(session):
if session.query(Dapp).count() != 0 or not SEED_DAPPS.exists():
return
try:
data = json.loads(SEED_DAPPS.read_text(encoding="utf-8"))
except Exception as e:
logger.warning("dapps.json unreadable: %r", e)
return
rows = data if isinstance(data, list) else data.get("dapps", data)
seen = {}
for rec in rows:
if not isinstance(rec, dict) or rec.get("model") != "dapps.dapps":
continue
f = rec.get("fields", {})
image = (f.get("image") or "").strip()
mirrored = _mirror_image(image, seen) if image else None
session.add(
Dapp(
id=int(rec.get("pk") or 0),
name=f.get("name") or "",
description=f.get("description"),
link=f.get("link"),
image=mirrored or image,
is_live=f.get("is_live") in (1, "1", "true", True),
counter=int(f.get("counter") or 0),
category_id=f.get("category_id"),
is_available_indonesia=f.get("is_available_indonesia"),
created_at=f.get("created_at"),
updated_at=f.get("updated_at"),
)
)
def list_tokens():
session = SessionLocal()
try:
return session.query(Token).order_by(Token.position.asc()).all()
finally:
session.close()
def get_version():
session = SessionLocal()
try:
return session.query(Version).filter(Version.id == 1).first()
finally:
session.close()
def list_dapps():
session = SessionLocal()
try:
return session.query(Dapp).order_by(Dapp.id.asc()).all()
finally:
session.close()
def increment_dapp_counter(dapp_id: int):
from sqlalchemy import text
with SessionLocal() as session:
row = session.execute(
text("UPDATE dapps SET counter = counter + 1 WHERE id = :dapp_id RETURNING counter"),
{"dapp_id": dapp_id},
).fetchone()
session.commit()
return row[0] if row else None
def list_wallets():
session = SessionLocal()
try:
return [w.account for w in session.query(Wallet).order_by(Wallet.id.asc()).all()]
finally:
session.close()