253 lines
7.7 KiB
Python
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() |