from datetime import datetime from typing import List, TypedDict, Optional, TYPE_CHECKING from sqlalchemy import and_ from sqlalchemy.orm import Mapped, aliased, mapped_column, relationship from app.brm.brn import BRN from app.extensions import db from app.models import AbstractConfiguration if TYPE_CHECKING: from app.models.bridges import BridgeConf from app.models.mirrors import Origin, Proxy, SmartProxy, StaticOrigin from app.models.onions import Eotk, Onion class GroupDict(TypedDict): Id: int GroupName: str Description: str ActiveOriginCount: int class Group(AbstractConfiguration): group_name: Mapped[str] = db.Column(db.String(80), unique=True, nullable=False) eotk: Mapped[bool] origins: Mapped[List["Origin"]] = relationship("Origin", back_populates="group") statics: Mapped[List["StaticOrigin"]] = relationship("StaticOrigin", back_populates="group") eotks: Mapped[List["Eotk"]] = relationship("Eotk", back_populates="group") onions: Mapped[List["Onion"]] = relationship("Onion", back_populates="group") smart_proxies: Mapped[List["SmartProxy"]] = relationship("SmartProxy", back_populates="group") pools: Mapped[List["Pool"]] = relationship("Pool", secondary="pool_group", back_populates="groups") @classmethod def csv_header(cls) -> List[str]: return super().csv_header() + [ "group_name", "eotk" ] @property def brn(self) -> BRN: return BRN( group_id=self.id, product="group", provider="", resource_type="group", resource_id=str(self.id) ) def to_dict(self) -> GroupDict: active_origins_query = ( db.session.query(aliased(Origin)) .filter(and_(Origin.group_id == self.id, Origin.destroyed.is_(None))) ) active_origins_count = active_origins_query.count() return { "Id": self.id, "GroupName": self.group_name, "Description": self.description, "ActiveOriginCount": active_origins_count, } class Pool(AbstractConfiguration): pool_name: Mapped[str] = mapped_column(db.String, unique=True) api_key: Mapped[str] redirector_domain: Mapped[Optional[str]] bridgeconfs: Mapped[List["BridgeConf"]] = relationship("BridgeConf", back_populates="pool") proxies: Mapped[List["Proxy"]] = relationship("Proxy", back_populates="pool") lists: Mapped[List["MirrorList"]] = relationship("MirrorList", back_populates="pool") groups: Mapped[List[Group]] = relationship("Group", secondary="pool_group", back_populates="pools") @classmethod def csv_header(cls) -> List[str]: return super().csv_header() + [ "pool_name" ] @property def brn(self) -> BRN: return BRN( group_id=0, product="pool", provider="", resource_type="pool", resource_id=str(self.pool_name) ) class PoolGroup(db.Model): # type: ignore[name-defined,misc] pool_id = db.Column(db.Integer, db.ForeignKey("pool.id"), primary_key=True) group_id = db.Column(db.Integer, db.ForeignKey("group.id"), primary_key=True) class MirrorList(AbstractConfiguration): pool_id = db.Column(db.Integer, db.ForeignKey("pool.id")) provider = db.Column(db.String(255), nullable=False) format = db.Column(db.String(20), nullable=False) encoding = db.Column(db.String(20), nullable=False) container = db.Column(db.String(255), nullable=False) branch = db.Column(db.String(255), nullable=False) role = db.Column(db.String(255), nullable=True) filename = db.Column(db.String(255), nullable=False) pool = db.relationship("Pool", back_populates="lists") providers_supported = { "github": "GitHub", "gitlab": "GitLab", "http_post": "HTTP POST", "s3": "AWS S3", } formats_supported = { "bc2": "Bypass Censorship v2", "bc3": "Bypass Censorship v3", "bca": "Bypass Censorship Analytics", "bridgelines": "Tor Bridge Lines", "rdr": "Redirector Data" } encodings_supported = { "json": "JSON (Plain)", "jsno": "JSON (Obfuscated)", "js": "JavaScript (Plain)", "jso": "JavaScript (Obfuscated)" } def destroy(self) -> None: self.destroyed = datetime.utcnow() self.updated = datetime.utcnow() def url(self) -> str: if self.provider == "gitlab": return f"https://gitlab.com/{self.container}/-/raw/{self.branch}/{self.filename}" if self.provider == "github": return f"https://raw.githubusercontent.com/{self.container}/{self.branch}/{self.filename}" if self.provider == "s3": return f"s3://{self.container}/{self.filename}" if self.provider == "http_post": return str(self.container) return "Unknown provider" @classmethod def csv_header(cls) -> List[str]: return super().csv_header() + [ "provider", "format", "container", "branch", "filename" ] @property def brn(self) -> BRN: return BRN( group_id=0, product="list", provider=self.provider, resource_type="list", resource_id=str(self.id) )