summaryrefslogtreecommitdiffstats
path: root/packages/meshbay-hub/src/meshbay_hub/hub_settings.py
blob: 1c2d2c4760509da8d8181ed313a1879f5ba4f2cb (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
"""
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)