feat: dapp registry + /api/getDapps, /img/dapps, DappAdmin; dapp-link verifier default-on (IDRS_VERIFY_DAPPS); is_live sticky override; fix edit-without-upload (Dapp+Token)
This commit is contained in:
1 parent
77d6cd5b92
commit
3b68820beb
9 files changed
+447
-51
No files matched your search
@@ -1,21 +1,70 @@
|
||||
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,
|
||||
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"))
|
||||
|
||||
|
||||
def init_db():
|
||||
Base.metadata.create_all(engine)
|
||||
_ensure_migrations(engine)
|
||||
seed()
|
||||
|
||||
|
||||
@@ -50,11 +99,93 @@ def seed():
|
||||
latestVersion=int(v["latestVersion"]),
|
||||
)
|
||||
)
|
||||
seed_dapps(session)
|
||||
session.commit()
|
||||
finally:
|
||||
session.close()
|
||||
|
||||
|
||||
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:
|
||||
@@ -67,5 +198,13 @@ 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()
|
||||
Reference in new issue
Block a user