""" Instance-wide settings stored in the `hub_settings` table. One reader per concern, so callers never touch raw strings or key names. A missing row means the built-in default — an upgrade never changes behaviour on its own, and a downgrade that drops the table just returns to defaults. """ from sqlalchemy.ext.asyncio import AsyncSession from meshbay_hub.db.models import HubSetting # Whether a member may create a group that is listed in the public directory and # open for anyone to join. Off makes the hub private-groups-only. ALLOW_PUBLIC_GROUPS = "allow_public_groups" _DEFAULTS: dict[str, str] = { ALLOW_PUBLIC_GROUPS: "true", } # ── Mail bounds ────────────────────────────────────────────────────────────── # # Stored here rather than read from `hub.toml` at each use, because an operator # has to be able to change them while the hub is serving: the hour the budget # runs out is exactly when nobody wants to edit a file and restart. The TOML # values are the defaults these fall back to, so an instance that never touches # the panel behaves as its configuration file says. MAIL_KEYS = ( "destination_cooldown_seconds", "destination_daily_cap", "hourly_budget", "hourly_reserved_for_recovery", "verification_resend_cooldown", "reset_cooldown", "email_change_cooldown", ) # What a value may be. A cooldown of zero disables it, which is a legitimate # thing for an operator to want; a budget of zero would stop the hub sending # anything at all, which is not, so those start at one. The upper bounds are # there because this is a number typed into a web form. MAIL_BOUNDS: dict[str, tuple[int, int]] = { "destination_cooldown_seconds": (0, 86_400), "destination_daily_cap": (1, 1_000), "hourly_budget": (1, 100_000), "hourly_reserved_for_recovery": (0, 100_000), "verification_resend_cooldown": (0, 86_400), "reset_cooldown": (0, 86_400), "email_change_cooldown": (0, 2_592_000), # 30 days } _mail_defaults: dict[str, int] = {} def set_mail_defaults(mail_cfg) -> None: """Record what `hub.toml` said. Called once, at startup.""" global _mail_defaults _mail_defaults = {k: int(getattr(mail_cfg, k)) for k in MAIL_KEYS} def mail_default(key: str) -> int: return _mail_defaults.get(key, 0) def clamp_mail_value(key: str, value: int) -> int: low, high = MAIL_BOUNDS[key] return max(low, min(high, int(value))) async def get_int(db: AsyncSession, key: str, fallback: int) -> int: raw = await get_raw(db, key) if raw is None: return fallback try: return int(raw) except ValueError: # A row that cannot be read is not a reason to send without a bound. return fallback async def mail_limits(db: AsyncSession) -> dict[str, int]: """Every mail bound, stored value or configured default.""" return {k: await get_int(db, f"mail.{k}", mail_default(k)) for k in MAIL_KEYS} async def get_raw(db: AsyncSession, key: str) -> str | None: row = await db.get(HubSetting, key) return row.value if row else None async def set_raw(db: AsyncSession, key: str, value: str) -> None: """Upsert. The caller owns the commit.""" row = await db.get(HubSetting, key) if row: row.value = value else: db.add(HubSetting(key=key, value=value)) async def get_bool(db: AsyncSession, key: str) -> bool: raw = await get_raw(db, key) if raw is None: raw = _DEFAULTS.get(key, "false") return raw == "true" async def public_groups_allowed(db: AsyncSession) -> bool: return await get_bool(db, ALLOW_PUBLIC_GROUPS)