from datetime import datetime, timezone from typing import TYPE_CHECKING, List, Optional, TypedDict 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] = mapped_column(unique=True) 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: if not TYPE_CHECKING: from app.models.mirrors import Origin # to prevent circular import 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: Mapped[int] = mapped_column(db.ForeignKey("pool.id"), primary_key=True) group_id: Mapped[int] = mapped_column(db.ForeignKey("group.id"), primary_key=True) class MirrorList(AbstractConfiguration): pool_id: Mapped[int] = mapped_column(db.ForeignKey("pool.id")) provider: Mapped[str] format: Mapped[str] encoding: Mapped[str] container: Mapped[str] branch: Mapped[str] role: Mapped[Optional[str]] filename: Mapped[str] 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.now(tz=timezone.utc) self.updated = datetime.now(tz=timezone.utc) 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), )