from datetime import datetime, timedelta, timezone from typing import Optional from flask import Blueprint, render_template, request from flask.typing import ResponseReturnValue from sqlalchemy import desc, or_ from app.models.activity import Activity from app.models.alarms import Alarm, AlarmState from app.models.bridges import Bridge from app.models.mirrors import Origin, Proxy from app.models.base import Group from app.portal.list import NewMirrorListForm from app.portal.automation import bp as automation from app.portal.bridgeconf import bp as bridgeconf from app.portal.bridge import bp as bridge from app.portal.eotk import bp as eotk from app.portal.group import bp as group from app.portal.list import bp as list_ from app.portal.origin import bp as origin from app.portal.onion import bp as onion from app.portal.proxy import bp as proxy from app.portal.webhook import bp as webhook portal = Blueprint("portal", __name__, template_folder="templates", static_folder="static") portal.register_blueprint(automation, url_prefix="/automation") portal.register_blueprint(bridgeconf, url_prefix="/bridgeconf") portal.register_blueprint(bridge, url_prefix="/bridge") portal.register_blueprint(eotk, url_prefix="/eotk") portal.register_blueprint(group, url_prefix="/group") portal.register_blueprint(list_, url_prefix="/list") portal.register_blueprint(origin, url_prefix="/origin") portal.register_blueprint(onion, url_prefix="/onion") portal.register_blueprint(proxy, url_prefix="/proxy") portal.register_blueprint(webhook, url_prefix="/webhook") @portal.app_template_filter("mirror_expiry") def calculate_mirror_expiry(s: datetime) -> str: expiry = s + timedelta(days=3) countdown = expiry - datetime.utcnow() if countdown.days == 0: return f"{countdown.seconds // 3600} hours" return f"{countdown.days} days" @portal.app_template_filter("format_datetime") def format_datetime(s: Optional[datetime]) -> str: if s is None: return "Unknown" return s.strftime("%a, %d %b %Y %H:%M:%S") def total_origins_blocked() -> int: count = 0 for o in Origin.query.filter(Origin.destroyed == None).all(): for a in o.alarms: if a.alarm_type.startswith("origin-block-ooni-"): if a.alarm_state == AlarmState.WARNING: count += 1 break return count @portal.route("/") def portal_home() -> ResponseReturnValue: groups = Group.query.order_by(Group.group_name).all() now = datetime.now(timezone.utc) proxies = Proxy.query.filter(Proxy.destroyed == None).all() last24 = len(Proxy.query.filter(Proxy.deprecated > (now - timedelta(days=1))).all()) last72 = len(Proxy.query.filter(Proxy.deprecated > (now - timedelta(days=3))).all()) lastweek = len(Proxy.query.filter(Proxy.deprecated > (now - timedelta(days=7))).all()) alarms = { s: len(Alarm.query.filter(Alarm.alarm_state == s.upper(), Alarm.last_updated > (now - timedelta(days=1))).all()) for s in ["critical", "warning", "ok", "unknown"] } bridges = Bridge.query.filter(Bridge.destroyed == None).all() br_last = { d: len(Bridge.query.filter(Bridge.deprecated > (now - timedelta(days=d))).all()) for d in [1, 3, 7] } activity = Activity.query.filter(Activity.added > (now - timedelta(days=2))).order_by(desc(Activity.added)).all() onionified = len([o for o in Origin.query.filter(Origin.destroyed == None).all() if o.onion() != None]) ooni_blocked = total_origins_blocked() total_origins = len(Origin.query.filter(Origin.destroyed == None).all()) return render_template("home.html.j2", section="home", groups=groups, last24=last24, last72=last72, lastweek=lastweek, proxies=proxies, **alarms, activity=activity, total_origins=total_origins, onionified=onionified, br_last=br_last, ooni_blocked=ooni_blocked, bridges=bridges) @portal.route("/search") def search() -> ResponseReturnValue: query = request.args.get("query") proxies = Proxy.query.filter(or_(Proxy.url.contains(query)), Proxy.destroyed == None).all() origins = Origin.query.filter(or_(Origin.description.contains(query), Origin.domain_name.contains(query))).all() return render_template("search.html.j2", section="home", proxies=proxies, origins=origins) @portal.route('/alarms') def view_alarms() -> ResponseReturnValue: one_day_ago = datetime.now(timezone.utc) - timedelta(days=1) alarms = Alarm.query.filter(Alarm.last_updated >= one_day_ago).order_by( desc(Alarm.alarm_state), desc(Alarm.state_changed)).all() return render_template("list.html.j2", section="alarm", title="Alarms", items=alarms)