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()