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
+129
-2
@@ -1,24 +1,37 @@
|
||||
import os
|
||||
import re
|
||||
from datetime import datetime, timezone
|
||||
from pathlib import Path
|
||||
|
||||
from sqladmin import Admin, ModelView
|
||||
from starlette.datastructures import UploadFile
|
||||
from starlette.requests import Request
|
||||
from wtforms import BooleanField, FileField, Form, IntegerField, StringField
|
||||
from wtforms import BooleanField, FileField, Form, IntegerField, SelectField, StringField
|
||||
from wtforms.validators import DataRequired
|
||||
|
||||
from sqlalchemy import text
|
||||
|
||||
from app.auth import AdminAuthBackend, get_session_secret
|
||||
from app.models import ICON_DIR, SessionLocal, Token, Version
|
||||
from app.models import ICON_DIR, SessionLocal, Dapp, Token, Version
|
||||
|
||||
BASE_DIR = Path(__file__).resolve().parent.parent
|
||||
TEMPLATES_DIR = str(BASE_DIR / "app" / "templates")
|
||||
|
||||
MAX_ICON_BYTES = 512 * 1024
|
||||
MAX_DAPP_IMAGE_BYTES = 2 * 1024 * 1024
|
||||
PNG_MAGIC = b"\x89PNG\r\n\x1a\n"
|
||||
PUBLIC_BASE_URL = os.environ.get("IDRS_PUBLIC_URL", "https://idrs.databisnis.id").rstrip("/")
|
||||
|
||||
DAPP_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"),
|
||||
]
|
||||
|
||||
|
||||
def _is_authenticated(request: Request) -> bool:
|
||||
return bool(request.session.get("auth"))
|
||||
@@ -46,6 +59,30 @@ def _next_token_id() -> int:
|
||||
return int(session.execute(text("SELECT COALESCE(MAX(idfToken), 0) + 1 FROM tokens")).scalar())
|
||||
|
||||
|
||||
def _now_iso() -> str:
|
||||
return datetime.now(timezone.utc).isoformat(timespec="seconds")
|
||||
|
||||
|
||||
def _sanitize_dapp_image_name(name: str) -> str:
|
||||
base = re.sub(r"[^A-Za-z0-9._-]+", "_", Path(name).name).strip("._") or "dapp"
|
||||
return base[:80]
|
||||
|
||||
|
||||
async def _extract_dapp_image(raw) -> bytes:
|
||||
if isinstance(raw, UploadFile):
|
||||
return await raw.read(MAX_DAPP_IMAGE_BYTES + 1)
|
||||
stream = raw.stream if hasattr(raw, "stream") else raw
|
||||
data = stream.read(MAX_DAPP_IMAGE_BYTES + 1)
|
||||
return data if isinstance(data, bytes) else bytes(data)
|
||||
|
||||
|
||||
def _sniff_image_extension(content: bytes) -> str | None:
|
||||
for magic, ext in DAPP_IMAGE_MAGIC:
|
||||
if content.startswith(magic):
|
||||
return ext
|
||||
return None
|
||||
|
||||
|
||||
class NullableIntegerField(IntegerField):
|
||||
def process_formdata(self, valuelist):
|
||||
if valuelist and valuelist[0].strip() == "":
|
||||
@@ -130,6 +167,95 @@ class TokenAdmin(ModelView, model=Token):
|
||||
data["iconUrl"] = f"{PUBLIC_BASE_URL}/img/token/{filename}"
|
||||
|
||||
|
||||
class DappForm(Form):
|
||||
name = StringField("Name", validators=[DataRequired()])
|
||||
description = StringField("Description")
|
||||
link = StringField("Link")
|
||||
image = StringField("Image URL")
|
||||
imageFile = FileField("Image file")
|
||||
is_live = BooleanField("Live")
|
||||
counter = IntegerField("Counter", default=0)
|
||||
category_id = NullableIntegerField("Category ID")
|
||||
is_available_indonesia = SelectField(
|
||||
"Available in Indonesia",
|
||||
choices=[("", "—"), ("true", "Yes"), ("false", "No")],
|
||||
default="",
|
||||
)
|
||||
|
||||
|
||||
class DappAdmin(ModelView, model=Dapp):
|
||||
name = "Dapps"
|
||||
icon = "fa-solid fa-globe"
|
||||
form = DappForm
|
||||
column_list = [
|
||||
"name",
|
||||
"link",
|
||||
"image",
|
||||
"is_live",
|
||||
"counter",
|
||||
"category_id",
|
||||
"is_available_indonesia",
|
||||
]
|
||||
column_labels = {
|
||||
"is_live": "Live",
|
||||
"category_id": "Category",
|
||||
"is_available_indonesia": "Avail. ID",
|
||||
}
|
||||
column_details_list = [
|
||||
"id",
|
||||
"name",
|
||||
"description",
|
||||
"link",
|
||||
"image",
|
||||
"is_live",
|
||||
"counter",
|
||||
"category_id",
|
||||
"is_available_indonesia",
|
||||
"created_at",
|
||||
"updated_at",
|
||||
]
|
||||
column_searchable_list = ["name", "description", "link"]
|
||||
column_default_sort = [("id", True)]
|
||||
page_size = 25
|
||||
|
||||
def is_accessible(self, request: Request) -> bool:
|
||||
return _is_authenticated(request)
|
||||
|
||||
async def on_model_change(self, data, model, is_created, request):
|
||||
data.pop("id", None)
|
||||
data.pop("is_live_override", None)
|
||||
submitted_live = bool(data.get("is_live"))
|
||||
if is_created:
|
||||
data["is_live_override"] = not submitted_live
|
||||
elif submitted_live != bool(model.is_live):
|
||||
data["is_live_override"] = not submitted_live
|
||||
if data.get("is_available_indonesia") == "":
|
||||
data["is_available_indonesia"] = None
|
||||
elif data.get("is_available_indonesia") in ("true", "True", "1", 1, "y", "on"):
|
||||
data["is_available_indonesia"] = True
|
||||
elif data.get("is_available_indonesia") in ("false", "False", "0", 0, "n", "off"):
|
||||
data["is_available_indonesia"] = False
|
||||
raw = data.get("imageFile")
|
||||
data.pop("imageFile", None)
|
||||
if raw:
|
||||
content = await _extract_dapp_image(raw)
|
||||
if not content:
|
||||
return
|
||||
if len(content) > MAX_DAPP_IMAGE_BYTES:
|
||||
raise Exception(f"Image must be ≤ {MAX_DAPP_IMAGE_BYTES // 1024}KB")
|
||||
ext = _sniff_image_extension(content)
|
||||
if not ext:
|
||||
raise Exception("Image must be a real image (PNG/JPEG/GIF/WebP/ICO/BMP)")
|
||||
filename = f"{Path(_sanitize_dapp_image_name(getattr(raw, 'filename', '') or 'dapp')).stem}{ext}"
|
||||
dapp_dir = ICON_DIR / "dapps"
|
||||
dapp_dir.mkdir(parents=True, exist_ok=True)
|
||||
(dapp_dir / filename).write_bytes(content)
|
||||
data["image"] = f"{PUBLIC_BASE_URL}/img/dapps/{filename}"
|
||||
if is_created:
|
||||
data["created_at"] = data.get("created_at") or _now_iso()
|
||||
data["updated_at"] = _now_iso()
|
||||
|
||||
|
||||
class VersionAdmin(ModelView, model=Version):
|
||||
name = "Android Version"
|
||||
icon = "fa-solid fa-mobile-screen"
|
||||
@@ -167,4 +293,5 @@ def create_admin(app):
|
||||
)
|
||||
admin.add_view(TokenAdmin)
|
||||
admin.add_view(VersionAdmin)
|
||||
admin.add_view(DappAdmin)
|
||||
return admin
|
||||
@@ -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()
|
||||
+46
-3
@@ -1,4 +1,5 @@
|
||||
from pathlib import Path
|
||||
import mimetypes
|
||||
|
||||
from fastapi import FastAPI, HTTPException
|
||||
from fastapi.responses import FileResponse
|
||||
@@ -6,12 +7,13 @@ from fastapi.staticfiles import StaticFiles
|
||||
|
||||
from app import db
|
||||
from app.admin import create_admin
|
||||
from app.verify import Verifier
|
||||
from app.verify import DappVerifier, TokenVerifier
|
||||
|
||||
BASE_DIR = Path(__file__).resolve().parent
|
||||
ICON_DIR = BASE_DIR / "static" / "icons"
|
||||
|
||||
verifier = Verifier()
|
||||
token_verifier = TokenVerifier()
|
||||
dapp_verifier = DappVerifier()
|
||||
|
||||
app = FastAPI(title="IDRS API")
|
||||
app.mount("/static", StaticFiles(directory=str(BASE_DIR / "static")), name="static")
|
||||
@@ -59,6 +61,14 @@ def _serve_icon(filename: str):
|
||||
return FileResponse(path, media_type="image/png")
|
||||
|
||||
|
||||
def _serve_dapp_image(filename: str):
|
||||
dapp_dir = ICON_DIR / "dapps"
|
||||
path = (dapp_dir / filename).resolve()
|
||||
if not path.is_file() or dapp_dir.resolve() not in path.parents:
|
||||
raise HTTPException(status_code=404, detail="not found")
|
||||
return FileResponse(path, media_type=mimetypes.guess_type(path.name)[0] or "application/octet-stream")
|
||||
|
||||
|
||||
@app.get("/img/token/{filename}")
|
||||
def get_icon(filename: str):
|
||||
return _serve_icon(filename)
|
||||
@@ -69,7 +79,40 @@ def get_public_icon(filename: str):
|
||||
return _serve_icon(filename)
|
||||
|
||||
|
||||
@app.get("/img/dapps/{filename}")
|
||||
def get_dapp_image(filename: str):
|
||||
return _serve_dapp_image(filename)
|
||||
|
||||
|
||||
@app.get("/public/img/dapps/{filename}")
|
||||
def get_public_dapp_image(filename: str):
|
||||
return _serve_dapp_image(filename)
|
||||
|
||||
|
||||
@app.get("/api/getDapps")
|
||||
def get_dapps():
|
||||
dapps = []
|
||||
for d in db.list_dapps():
|
||||
dapps.append(
|
||||
{
|
||||
"id": d.id,
|
||||
"name": d.name,
|
||||
"description": d.description,
|
||||
"link": d.link,
|
||||
"image": d.image,
|
||||
"is_live": bool(d.is_live),
|
||||
"counter": d.counter,
|
||||
"category_id": d.category_id,
|
||||
"is_available_indonesia": d.is_available_indonesia,
|
||||
"created_at": d.created_at,
|
||||
"updated_at": d.updated_at,
|
||||
}
|
||||
)
|
||||
return {"dappList": dapps}
|
||||
|
||||
|
||||
@app.on_event("startup")
|
||||
def startup():
|
||||
db.init_db()
|
||||
verifier.start()
|
||||
token_verifier.start()
|
||||
dapp_verifier.start()
|
||||
+20
-1
@@ -39,6 +39,7 @@ class Token(Base):
|
||||
iconUrl = Column("iconUrl", String, nullable=False, default="")
|
||||
filename = Column("filename", String, nullable=False, default="")
|
||||
is_live = Column("is_live", Boolean, nullable=False, default=True)
|
||||
icon = None
|
||||
|
||||
|
||||
class Version(Base):
|
||||
@@ -48,4 +49,22 @@ class Version(Base):
|
||||
idVersion = Column("idVersion", Integer, nullable=False)
|
||||
appName = Column("appName", String, nullable=False)
|
||||
acceptableVersion = Column("acceptableVersion", Integer, nullable=False)
|
||||
latestVersion = Column("latestVersion", Integer, nullable=False)
|
||||
latestVersion = Column("latestVersion", Integer, nullable=False)
|
||||
|
||||
|
||||
class Dapp(Base):
|
||||
__tablename__ = "dapps"
|
||||
|
||||
id = Column(Integer, primary_key=True)
|
||||
name = Column("name", String, nullable=False)
|
||||
description = Column("description", String, nullable=True)
|
||||
link = Column("link", String, nullable=True)
|
||||
image = Column("image", String, nullable=True)
|
||||
is_live = Column("is_live", Boolean, nullable=False, default=True)
|
||||
counter = Column("counter", Integer, nullable=False, default=0)
|
||||
category_id = Column("category_id", Integer, nullable=True)
|
||||
is_available_indonesia = Column("is_available_indonesia", Boolean, nullable=True)
|
||||
created_at = Column("created_at", String, nullable=True)
|
||||
updated_at = Column("updated_at", String, nullable=True)
|
||||
is_live_override = Column("is_live_override", Boolean, nullable=False, default=False)
|
||||
imageFile = None
|
||||
+87
-31
@@ -1,17 +1,16 @@
|
||||
import logging
|
||||
import os
|
||||
import threading
|
||||
import time
|
||||
import urllib.error
|
||||
import urllib.request
|
||||
from pathlib import Path
|
||||
from urllib.parse import urlparse
|
||||
|
||||
from app.models import ICON_DIR, SessionLocal, Token
|
||||
from app.models import ICON_DIR, SessionLocal, Dapp, Token
|
||||
|
||||
logger = logging.getLogger("idrs.verify")
|
||||
|
||||
ENABLED = os.environ.get("IDRS_VERIFY_ENABLED", "").strip().lower() in ("1", "true", "yes", "on")
|
||||
TOKENS_ENABLED = os.environ.get("IDRS_VERIFY_TOKENS", "").strip().lower() in ("1", "true", "yes", "on")
|
||||
DAPPS_ENABLED = os.environ.get("IDRS_VERIFY_DAPPS", "1").strip().lower() not in ("0", "false", "off", "no")
|
||||
PUBLIC_BASE_URL = os.environ.get("IDRS_PUBLIC_URL", "https://idrs.databisnis.id").rstrip("/")
|
||||
PUBLIC_HOST = urlparse(PUBLIC_BASE_URL).hostname or "idrs.databisnis.id"
|
||||
INTERVAL = int(os.environ.get("IDRS_VERIFY_INTERVAL", "600"))
|
||||
@@ -26,31 +25,25 @@ def _local_reachable(filename: str) -> bool:
|
||||
return path.is_file() and token_dir.resolve() in path.parents
|
||||
|
||||
|
||||
def _http_reachable(url: str) -> bool:
|
||||
req = urllib.request.Request(url, headers={"User-Agent": USER_AGENT, "Accept": "image/*,*/*"})
|
||||
def _http_reachable(url: str, accept: str = "image/*,*/*") -> bool:
|
||||
req = urllib.request.Request(
|
||||
url, headers={"User-Agent": USER_AGENT, "Accept": accept}
|
||||
)
|
||||
try:
|
||||
with urllib.request.urlopen(req, timeout=HTTP_TIMEOUT) as resp:
|
||||
return 200 <= resp.status < 300
|
||||
except urllib.error.HTTPError as e:
|
||||
logger.warning("icon http %s for %s", e.code, url)
|
||||
logger.warning("http %s for %s", e.code, url)
|
||||
return False
|
||||
except Exception as e:
|
||||
logger.warning("icon network error %r for %s", e, url)
|
||||
logger.warning("network error %r for %s", e, url)
|
||||
return False
|
||||
|
||||
|
||||
def reachable(token) -> bool:
|
||||
url = (token.iconUrl or "").strip()
|
||||
if not url:
|
||||
return False
|
||||
if urlparse(url).hostname == PUBLIC_HOST:
|
||||
return _local_reachable(token.filename or "")
|
||||
return _http_reachable(url)
|
||||
|
||||
|
||||
class Verifier(threading.Thread):
|
||||
def __init__(self):
|
||||
super().__init__(daemon=True, name="idrs-verify")
|
||||
def __init__(self, name: str, enabled: bool):
|
||||
super().__init__(daemon=True, name=name)
|
||||
self._enabled = enabled
|
||||
self._stop = threading.Event()
|
||||
self._fail_counts = {}
|
||||
|
||||
@@ -58,8 +51,8 @@ class Verifier(threading.Thread):
|
||||
self._stop.set()
|
||||
|
||||
def start(self):
|
||||
if not ENABLED:
|
||||
logger.info("verifier disabled (IDRS_VERIFY_ENABLED not set)")
|
||||
if not self._enabled:
|
||||
logger.info("%s disabled", self.name)
|
||||
return
|
||||
super().start()
|
||||
|
||||
@@ -68,24 +61,87 @@ class Verifier(threading.Thread):
|
||||
try:
|
||||
self._check_once()
|
||||
except Exception:
|
||||
logger.exception("verify pass failed")
|
||||
logger.exception("%s pass failed", self.name)
|
||||
self._stop.wait(INTERVAL)
|
||||
|
||||
def _check_once(self):
|
||||
session = SessionLocal()
|
||||
try:
|
||||
for token in session.query(Token).all():
|
||||
ok = reachable(token)
|
||||
for item in self._items(session):
|
||||
if self._skip(item):
|
||||
continue
|
||||
ok = self._reachable(item)
|
||||
key = self._key(item)
|
||||
if ok:
|
||||
self._fail_counts[token.idfToken] = 0
|
||||
self._fail_counts[key] = 0
|
||||
live = True
|
||||
else:
|
||||
fails = self._fail_counts.get(token.idfToken, 0) + 1
|
||||
self._fail_counts[token.idfToken] = fails
|
||||
fails = self._fail_counts.get(key, 0) + 1
|
||||
self._fail_counts[key] = fails
|
||||
live = fails < GRACE_PASSES
|
||||
if bool(token.is_live) != live:
|
||||
token.is_live = live
|
||||
logger.info("token %s is_live -> %s (%s)", token.idfToken, live, token.iconUrl)
|
||||
if bool(item.is_live) != live:
|
||||
item.is_live = live
|
||||
logger.info("%s is_live -> %s (%s)", key, live, self._label(item))
|
||||
session.commit()
|
||||
finally:
|
||||
session.close()
|
||||
session.close()
|
||||
|
||||
def _items(self, session):
|
||||
raise NotImplementedError
|
||||
|
||||
def _key(self, item):
|
||||
raise NotImplementedError
|
||||
|
||||
def _label(self, item):
|
||||
raise NotImplementedError
|
||||
|
||||
def _reachable(self, item) -> bool:
|
||||
raise NotImplementedError
|
||||
|
||||
def _skip(self, item) -> bool:
|
||||
return False
|
||||
|
||||
|
||||
class TokenVerifier(Verifier):
|
||||
def __init__(self):
|
||||
super().__init__(name="idrs-verify-tokens", enabled=TOKENS_ENABLED)
|
||||
|
||||
def _items(self, session):
|
||||
return session.query(Token).all()
|
||||
|
||||
def _key(self, item):
|
||||
return item.idfToken
|
||||
|
||||
def _label(self, item):
|
||||
return item.iconUrl or ""
|
||||
|
||||
def _reachable(self, item) -> bool:
|
||||
url = (item.iconUrl or "").strip()
|
||||
if not url:
|
||||
return False
|
||||
if urlparse(url).hostname == PUBLIC_HOST:
|
||||
return _local_reachable(item.filename or "")
|
||||
return _http_reachable(url, "image/*,*/*")
|
||||
|
||||
|
||||
class DappVerifier(Verifier):
|
||||
def __init__(self):
|
||||
super().__init__(name="idrs-verify-dapps", enabled=DAPPS_ENABLED)
|
||||
|
||||
def _items(self, session):
|
||||
return session.query(Dapp).all()
|
||||
|
||||
def _key(self, item):
|
||||
return item.id
|
||||
|
||||
def _label(self, item):
|
||||
return item.link or ""
|
||||
|
||||
def _reachable(self, item) -> bool:
|
||||
url = (item.link or "").strip()
|
||||
if not url:
|
||||
return False
|
||||
return _http_reachable(url, "text/html,application/xhtml+xml,*/*")
|
||||
|
||||
def _skip(self, item) -> bool:
|
||||
return bool(getattr(item, "is_live_override", False))
|
||||
Reference in new issue
Block a user