summaryrefslogtreecommitdiffstats
path: root/packages/meshbay-hub/src/meshbay_hub
diff options
context:
space:
mode:
Diffstat (limited to 'packages/meshbay-hub/src/meshbay_hub')
-rw-r--r--packages/meshbay-hub/src/meshbay_hub/api/admin.py5
-rw-r--r--packages/meshbay-hub/src/meshbay_hub/api/deps.py2
-rw-r--r--packages/meshbay-hub/src/meshbay_hub/api/federation.py12
-rw-r--r--packages/meshbay-hub/src/meshbay_hub/api/groups.py21
-rw-r--r--packages/meshbay-hub/src/meshbay_hub/api/hub.py2
-rw-r--r--packages/meshbay-hub/src/meshbay_hub/api/moderation.py1
-rw-r--r--packages/meshbay-hub/src/meshbay_hub/api/nodes.py10
-rw-r--r--packages/meshbay-hub/src/meshbay_hub/api/notifications.py6
-rw-r--r--packages/meshbay-hub/src/meshbay_hub/api/revocation.py20
-rw-r--r--packages/meshbay-hub/src/meshbay_hub/api/signaling.py4
-rw-r--r--packages/meshbay-hub/src/meshbay_hub/api/users.py49
-rw-r--r--packages/meshbay-hub/src/meshbay_hub/app.py42
-rw-r--r--packages/meshbay-hub/src/meshbay_hub/auth.py4
-rw-r--r--packages/meshbay-hub/src/meshbay_hub/csam.py5
-rw-r--r--packages/meshbay-hub/src/meshbay_hub/db/__init__.py4
-rw-r--r--packages/meshbay-hub/src/meshbay_hub/db/migrations/env.py6
-rw-r--r--packages/meshbay-hub/src/meshbay_hub/db/migrations/versions/a7b8c9d0e1f2_add_group_last_activity.py11
-rw-r--r--packages/meshbay-hub/src/meshbay_hub/db/migrations/versions/a9b8c7d6e5f4_add_login_throttle.py8
-rw-r--r--packages/meshbay-hub/src/meshbay_hub/db/migrations/versions/b1c2d3e4f5a6_add_hub_settings.py11
-rw-r--r--packages/meshbay-hub/src/meshbay_hub/db/migrations/versions/c3d4e5f6a7b8_group_name_unique_per_owner.py11
-rw-r--r--packages/meshbay-hub/src/meshbay_hub/db/migrations/versions/d28b9caf9f07_initial_schema.py11
-rw-r--r--packages/meshbay-hub/src/meshbay_hub/db/migrations/versions/d4e5f6a7b8c9_add_email_verification.py11
-rw-r--r--packages/meshbay-hub/src/meshbay_hub/db/migrations/versions/e5f6a7b8c9d0_add_mail_quota.py8
-rw-r--r--packages/meshbay-hub/src/meshbay_hub/db/migrations/versions/f1a2b3c4d5e6_add_user_preferences.py11
-rw-r--r--packages/meshbay-hub/src/meshbay_hub/db/models.py14
-rw-r--r--packages/meshbay-hub/src/meshbay_hub/login_throttle.py10
-rw-r--r--packages/meshbay-hub/src/meshbay_hub/mail.py24
-rw-r--r--packages/meshbay-hub/src/meshbay_hub/tasks/cleanup.py10
28 files changed, 173 insertions, 160 deletions
diff --git a/packages/meshbay-hub/src/meshbay_hub/api/admin.py b/packages/meshbay-hub/src/meshbay_hub/api/admin.py
index 7ca1e68..d8e13e9 100644
--- a/packages/meshbay-hub/src/meshbay_hub/api/admin.py
+++ b/packages/meshbay-hub/src/meshbay_hub/api/admin.py
@@ -6,19 +6,18 @@ Separate from moderation.py (which handles public reporting and content blocklis
"""
import logging
-from datetime import datetime, timezone
from fastapi import APIRouter, Depends, HTTPException, Query
from pydantic import BaseModel
from sqlalchemy import func, select
from sqlalchemy.ext.asyncio import AsyncSession
-from meshbay_hub.auth import decrypt_email
+from meshbay_hub import hub_settings
from meshbay_hub.api.deps import require_admin, require_moderator, user_is_admin
from meshbay_hub.api.revocation import get_connected_node_count, is_node_connected
+from meshbay_hub.auth import decrypt_email
from meshbay_hub.db.engine import get_db
from meshbay_hub.db.models import Group, GroupMember, IPLog, Node, User
-from meshbay_hub import hub_settings
log = logging.getLogger(__name__)
diff --git a/packages/meshbay-hub/src/meshbay_hub/api/deps.py b/packages/meshbay-hub/src/meshbay_hub/api/deps.py
index 907a481..501be9d 100644
--- a/packages/meshbay-hub/src/meshbay_hub/api/deps.py
+++ b/packages/meshbay-hub/src/meshbay_hub/api/deps.py
@@ -8,8 +8,8 @@ JWT scope enforcement:
"""
from fastapi import Depends, Header, HTTPException, status
-from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy import select
+from sqlalchemy.ext.asyncio import AsyncSession
from meshbay_hub.auth import decode_access_token
from meshbay_hub.db.engine import get_db
diff --git a/packages/meshbay-hub/src/meshbay_hub/api/federation.py b/packages/meshbay-hub/src/meshbay_hub/api/federation.py
index 9e252c6..755639e 100644
--- a/packages/meshbay-hub/src/meshbay_hub/api/federation.py
+++ b/packages/meshbay-hub/src/meshbay_hub/api/federation.py
@@ -23,18 +23,18 @@ Protocol version: MHP 0.1
import logging
import time
import uuid
+from datetime import UTC
import jwt
-from fastapi import APIRouter, Depends, HTTPException, Header
+from fastapi import APIRouter, Depends, Header, HTTPException
+from meshbay_common import MHP_VERSION
from pydantic import BaseModel
from sqlalchemy import func, select
from sqlalchemy.ext.asyncio import AsyncSession
-from meshbay_common import MHP_VERSION
from meshbay_hub import __version__, hub_settings
from meshbay_hub.api.deps import require_admin
-from meshbay_hub.auth import (
- hub_id, hub_private_key_pem, hub_public_key_pem)
+from meshbay_hub.auth import hub_id, hub_private_key_pem, hub_public_key_pem
from meshbay_hub.db.engine import get_db
from meshbay_hub.db.models import FederatedGroup, Group, HubPeer, User
@@ -244,8 +244,8 @@ async def receive_directory(
if len(body.groups) > MAX_FEDERATED_GROUPS_PER_PUSH:
raise HTTPException(status_code=413, detail="Too many groups in one push")
- from datetime import datetime, timezone
- now = datetime.now(timezone.utc)
+ from datetime import datetime
+ now = datetime.now(UTC)
have = await db.scalar(
select(func.count()).select_from(FederatedGroup)
.where(FederatedGroup.source_hub == sender)) or 0
diff --git a/packages/meshbay-hub/src/meshbay_hub/api/groups.py b/packages/meshbay-hub/src/meshbay_hub/api/groups.py
index 72ba194..3a11345 100644
--- a/packages/meshbay-hub/src/meshbay_hub/api/groups.py
+++ b/packages/meshbay-hub/src/meshbay_hub/api/groups.py
@@ -1,22 +1,27 @@
"""Group endpoints — /v1/groups/*"""
import re
+from datetime import UTC, datetime
from fastapi import APIRouter, Depends, HTTPException, Query, Request
from pydantic import BaseModel
-from datetime import datetime, timezone
from sqlalchemy import func, or_, select, update
from sqlalchemy.exc import IntegrityError
from sqlalchemy.ext.asyncio import AsyncSession
from meshbay_hub import hub_settings, mail
-from meshbay_hub.auth import decrypt_email
from meshbay_hub.api.deps import get_current_user, require_user_scope
from meshbay_hub.api.middleware import limiter
from meshbay_hub.api.netutil import client_ip
+from meshbay_hub.auth import decrypt_email
from meshbay_hub.db.engine import get_db
from meshbay_hub.db.models import (
- FederatedGroup, Group, GroupMember, IPLog, SwarmSource, User,
+ FederatedGroup,
+ Group,
+ GroupMember,
+ IPLog,
+ SwarmSource,
+ User,
)
router = APIRouter(prefix="/v1/groups", tags=["groups"])
@@ -97,7 +102,7 @@ async def touch_group_activity(
await db.execute(
update(Group)
.where(Group.id == group_id)
- .values(last_activity_at=datetime.now(timezone.utc)))
+ .values(last_activity_at=datetime.now(UTC)))
await db.commit()
return {"ok": True}
@@ -261,9 +266,9 @@ async def swarm_register(
detail="endpoint must be '<webrtc|quic>:<port>' — a port on the "
"registering node, not an address")
- from datetime import datetime, timezone
+ from datetime import datetime
existing = await db.get(SwarmSource, (body.content_hash, current_user.id))
- now = datetime.now(timezone.utc)
+ now = datetime.now(UTC)
if existing:
existing.endpoint = body.endpoint
existing.last_seen = now
@@ -297,8 +302,8 @@ async def swarm_sources(
Authenticated (H7): an open endpoint lets anyone probe whether a given file
exists anywhere in the network and which node holds it.
"""
- from datetime import datetime, timezone, timedelta
- cutoff = datetime.now(timezone.utc) - timedelta(minutes=30)
+ from datetime import datetime, timedelta
+ cutoff = datetime.now(UTC) - timedelta(minutes=30)
result = await db.execute(
select(SwarmSource)
.where(
diff --git a/packages/meshbay-hub/src/meshbay_hub/api/hub.py b/packages/meshbay-hub/src/meshbay_hub/api/hub.py
index 94e9b3c..cab60c8 100644
--- a/packages/meshbay-hub/src/meshbay_hub/api/hub.py
+++ b/packages/meshbay-hub/src/meshbay_hub/api/hub.py
@@ -1,9 +1,9 @@
"""Hub info endpoints — /v1/hub/*"""
from fastapi import APIRouter, Depends
+from meshbay_common import MHP_VERSION, MNP_VERSION
from sqlalchemy.ext.asyncio import AsyncSession
-from meshbay_common import MNP_VERSION, MHP_VERSION
from meshbay_hub import __version__, hub_settings
from meshbay_hub.api import federation
from meshbay_hub.auth import hub_public_key_pem
diff --git a/packages/meshbay-hub/src/meshbay_hub/api/moderation.py b/packages/meshbay-hub/src/meshbay_hub/api/moderation.py
index ee10cbc..35c688c 100644
--- a/packages/meshbay-hub/src/meshbay_hub/api/moderation.py
+++ b/packages/meshbay-hub/src/meshbay_hub/api/moderation.py
@@ -23,7 +23,6 @@ Node integration:
"""
import logging
-from datetime import datetime, timezone
from fastapi import APIRouter, Depends, HTTPException, Query, Request
from pydantic import BaseModel
diff --git a/packages/meshbay-hub/src/meshbay_hub/api/nodes.py b/packages/meshbay-hub/src/meshbay_hub/api/nodes.py
index 83b60f2..7478173 100644
--- a/packages/meshbay-hub/src/meshbay_hub/api/nodes.py
+++ b/packages/meshbay-hub/src/meshbay_hub/api/nodes.py
@@ -1,20 +1,20 @@
"""Node endpoints — /v1/nodes/*"""
-from datetime import datetime, timezone
import base64
import time
+from datetime import UTC, datetime
-from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PublicKey
from cryptography.exceptions import InvalidSignature
+from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PublicKey
from fastapi import APIRouter, Depends, HTTPException, Request
from pydantic import BaseModel
from sqlalchemy import func, select
from sqlalchemy.ext.asyncio import AsyncSession
-from meshbay_hub.auth import issue_access_token
from meshbay_hub.api.deps import get_current_user
from meshbay_hub.api.middleware import limiter
from meshbay_hub.api.netutil import client_ip
+from meshbay_hub.auth import issue_access_token
from meshbay_hub.db.engine import get_db
from meshbay_hub.db.models import GroupMember, IPLog, Node, User
@@ -156,7 +156,7 @@ async def announce_node(
if node is not None:
node.endpoint_hint = body.endpoint_hint
node.observed_ip = seen_from
- node.last_seen = datetime.now(timezone.utc)
+ node.last_seen = datetime.now(UTC)
db.add(IPLog(user_id=current_user.id, event="node_announce",
ip_address=seen_from, detail=body.endpoint_hint))
await db.commit()
@@ -182,7 +182,7 @@ async def announce_node(
pk_node=body.pk_node,
endpoint_hint=body.endpoint_hint,
observed_ip=seen_from,
- last_seen=datetime.now(timezone.utc),
+ last_seen=datetime.now(UTC),
)
db.add(node)
db.add(IPLog(
diff --git a/packages/meshbay-hub/src/meshbay_hub/api/notifications.py b/packages/meshbay-hub/src/meshbay_hub/api/notifications.py
index b5783ab..d96ec18 100644
--- a/packages/meshbay-hub/src/meshbay_hub/api/notifications.py
+++ b/packages/meshbay-hub/src/meshbay_hub/api/notifications.py
@@ -19,9 +19,9 @@ migration for no gain, and `unread_only` stays because it is what an older
interface asks for and it still answers correctly — every row is unread.
"""
-from fastapi import APIRouter, Depends, HTTPException, Query
-from datetime import datetime, timezone
+from datetime import UTC, datetime
+from fastapi import APIRouter, Depends, HTTPException, Query
from sqlalchemy import delete, func, select
from sqlalchemy.ext.asyncio import AsyncSession
@@ -182,7 +182,7 @@ async def create_notification(
existing.detail = detail
existing.link = link
existing.read = False
- existing.created_at = datetime.now(timezone.utc)
+ existing.created_at = datetime.now(UTC)
await db.flush()
return existing
diff --git a/packages/meshbay-hub/src/meshbay_hub/api/revocation.py b/packages/meshbay-hub/src/meshbay_hub/api/revocation.py
index 1f1f5c5..f8cae8a 100644
--- a/packages/meshbay-hub/src/meshbay_hub/api/revocation.py
+++ b/packages/meshbay-hub/src/meshbay_hub/api/revocation.py
@@ -26,23 +26,21 @@ and close active connections for that user.
"""
import asyncio
-import base64
import json
import logging
import time
import uuid
-from datetime import datetime, timezone
-from typing import Any
+from datetime import UTC, datetime
+import jwt
from fastapi import APIRouter, Depends, HTTPException, Request, WebSocket, WebSocketDisconnect
+from meshbay_common.background import spawn
from pydantic import BaseModel
from sqlalchemy import select, update
from sqlalchemy.ext.asyncio import AsyncSession
-import jwt
-from meshbay_common.background import spawn
-from meshbay_hub.auth import hub_public_key_pem, decode_access_token
from meshbay_hub.api.deps import get_current_user, require_admin
+from meshbay_hub.auth import decode_access_token
from meshbay_hub.db.engine import get_db
from meshbay_hub.db.models import Group, GroupMember, IPLog, Node, User
@@ -143,7 +141,7 @@ async def _mark_hosted(group_ids: list[str]) -> None:
await db.execute(
update(Group)
.where(Group.id.in_(group_ids), Group.hosted_at.is_(None))
- .values(hosted_at=datetime.now(timezone.utc)))
+ .values(hosted_at=datetime.now(UTC)))
await db.commit()
except Exception as e:
# A group that stays unhosted in the table is visible to its owner and
@@ -169,7 +167,7 @@ async def broadcast_revocation(token: str) -> int:
def _sign_revocation(target: str, target_id: str, reason: str) -> str:
"""Issue a signed revocation token (JWT EdDSA)."""
- from meshbay_hub.auth import _hub_sk_pem, _hub_id
+ from meshbay_hub.auth import _hub_id, _hub_sk_pem
now = int(time.time())
payload = {
"type": "revocation",
@@ -208,9 +206,9 @@ async def _handle_chat_notify(group_id: str, sender_name: str, sender_user_id: s
(node_id or "?")[:8])
return
try:
- from meshbay_hub.db.engine import get_session_factory
- from meshbay_hub.db.models import GroupMember, Group
from meshbay_hub.api.notifications import create_notification
+ from meshbay_hub.db.engine import get_session_factory
+ from meshbay_hub.db.models import Group, GroupMember
async with get_session_factory()() as db:
group = await db.get(Group, group_id)
@@ -478,7 +476,7 @@ async def notify_incoming(
try:
await asyncio.wait_for(event.wait(), timeout=5.0)
- except asyncio.TimeoutError:
+ except TimeoutError:
raise HTTPException(status_code=504, detail="Node did not respond in time")
finally:
_punch_events.pop(node_id, None)
diff --git a/packages/meshbay-hub/src/meshbay_hub/api/signaling.py b/packages/meshbay-hub/src/meshbay_hub/api/signaling.py
index bc03b45..8a10822 100644
--- a/packages/meshbay-hub/src/meshbay_hub/api/signaling.py
+++ b/packages/meshbay-hub/src/meshbay_hub/api/signaling.py
@@ -25,8 +25,8 @@ from sqlalchemy.ext.asyncio import AsyncSession
from meshbay_hub import hub_settings
from meshbay_hub.api.deps import get_current_user
from meshbay_hub.api.middleware import limiter
-from meshbay_hub.db.engine import get_db
from meshbay_hub.api.netutil import client_ip
+from meshbay_hub.db.engine import get_db
from meshbay_hub.db.models import Group, GroupMember, IPLog, User
log = logging.getLogger(__name__)
@@ -167,7 +167,7 @@ async def webrtc_offer(
try:
answer = await asyncio.wait_for(answer_future, timeout=15.0)
- except asyncio.TimeoutError:
+ except TimeoutError:
raise HTTPException(
status_code=504, detail="Node did not respond with WebRTC answer")
diff --git a/packages/meshbay-hub/src/meshbay_hub/api/users.py b/packages/meshbay-hub/src/meshbay_hub/api/users.py
index 0394f53..ee8aabc 100644
--- a/packages/meshbay-hub/src/meshbay_hub/api/users.py
+++ b/packages/meshbay-hub/src/meshbay_hub/api/users.py
@@ -6,7 +6,7 @@ import re
import secrets
import time
import uuid
-from datetime import datetime, timedelta, timezone
+from datetime import UTC, datetime, timedelta
from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PublicKey
from fastapi import APIRouter, Depends, HTTPException, Request
@@ -33,8 +33,17 @@ from meshbay_hub.auth import (
from meshbay_hub.config import HubConfig
from meshbay_hub.db.engine import get_db
from meshbay_hub.db.models import (
- EmailVerification, Group, GroupMember, IPLog, Node, Notification,
- RefreshToken, SwarmSource, User, UserDevice, UserPreference,
+ EmailVerification,
+ Group,
+ GroupMember,
+ IPLog,
+ Node,
+ Notification,
+ RefreshToken,
+ SwarmSource,
+ User,
+ UserDevice,
+ UserPreference,
)
log = logging.getLogger(__name__)
@@ -61,7 +70,7 @@ async def _refresh_expiry(db: AsyncSession, family_id: str | None = None) -> dat
renewals of a tab that is being used.
"""
limits = await hub_settings.session_limits(db)
- now = datetime.now(timezone.utc)
+ now = datetime.now(UTC)
idle = max(limits["refresh_idle_hours"] * 3600, _ttl() + 3600)
started = now
if family_id is not None:
@@ -69,7 +78,7 @@ async def _refresh_expiry(db: AsyncSession, family_id: str | None = None) -> dat
select(func.min(RefreshToken.created_at))
.where(RefreshToken.family_id == family_id))
if first is not None:
- started = first if first.tzinfo else first.replace(tzinfo=timezone.utc)
+ started = first if first.tzinfo else first.replace(tzinfo=UTC)
return min(now + timedelta(seconds=idle),
started + timedelta(hours=limits["max_hours"]))
@@ -193,7 +202,7 @@ async def register(
EmailVerification.user_id == found.id,
EmailVerification.purpose == "registration",
EmailVerification.created_at
- > datetime.now(timezone.utc)
+ > datetime.now(UTC)
- timedelta(seconds=resend_cooldown),
))
if not recent.first():
@@ -270,7 +279,7 @@ async def _create_and_send_verification(
code=code,
purpose="registration",
user_id=user.id,
- expires_at=datetime.now(timezone.utc) + timedelta(seconds=VERIFICATION_TTL),
+ expires_at=datetime.now(UTC) + timedelta(seconds=VERIFICATION_TTL),
))
await db.flush()
await mail.send_off_loop(
@@ -292,7 +301,7 @@ async def verify_email(
):
"""Verify a registration email with the code received by mail."""
eh = hash_email_blind(body.email)
- now = datetime.now(timezone.utc)
+ now = datetime.now(UTC)
result = await db.execute(
select(EmailVerification).where(
@@ -306,7 +315,7 @@ async def verify_email(
raise HTTPException(status_code=404,
detail="No pending verification for this email")
- if verif.expires_at.replace(tzinfo=timezone.utc) < now:
+ if verif.expires_at.replace(tzinfo=UTC) < now:
raise HTTPException(status_code=410, detail="Verification code expired")
if verif.attempts >= VERIFICATION_MAX_ATTEMPTS:
@@ -603,7 +612,7 @@ async def device_auth(
await db.commit()
raise HTTPException(status_code=401, detail="Invalid signature")
- matched.last_seen = datetime.now(timezone.utc)
+ matched.last_seen = datetime.now(UTC)
memberships = await db.execute(
select(GroupMember.group_id).where(GroupMember.user_id == user.id))
@@ -650,7 +659,7 @@ async def token_refresh(
await db.commit()
raise HTTPException(status_code=401, detail="Token reuse detected — family revoked")
- if rt.expires_at.replace(tzinfo=timezone.utc) < datetime.now(timezone.utc):
+ if rt.expires_at.replace(tzinfo=UTC) < datetime.now(UTC):
raise HTTPException(status_code=401, detail="Expired refresh token")
user = await db.get(User, rt.user_id)
@@ -659,7 +668,7 @@ async def token_refresh(
# The family's first sign-in was longer ago than any session may last.
expires_at = await _refresh_expiry(db, rt.family_id)
- if expires_at <= datetime.now(timezone.utc):
+ if expires_at <= datetime.now(UTC):
await db.execute(
update(RefreshToken)
.where(RefreshToken.family_id == rt.family_id)
@@ -779,7 +788,7 @@ async def update_profile(
cooldown = await hub_settings.get_int(
db, "mail.email_change_cooldown",
hub_settings.mail_default("email_change_cooldown"))
- since = datetime.now(timezone.utc) - timedelta(seconds=cooldown)
+ since = datetime.now(UTC) - timedelta(seconds=cooldown)
recent = await db.execute(
select(IPLog).where(
IPLog.user_id == current_user.id,
@@ -820,7 +829,7 @@ async def update_profile(
code=code,
purpose="email_change",
user_id=current_user.id,
- expires_at=datetime.now(timezone.utc) + timedelta(seconds=VERIFICATION_TTL),
+ expires_at=datetime.now(UTC) + timedelta(seconds=VERIFICATION_TTL),
))
db.add(IPLog(user_id=current_user.id, event="email_change_request",
ip_address=client_ip(request)))
@@ -860,7 +869,7 @@ async def verify_email_change(
db: AsyncSession = Depends(get_db),
):
"""Confirm an email change with the code sent to the new address."""
- now = datetime.now(timezone.utc)
+ now = datetime.now(UTC)
result = await db.execute(
select(EmailVerification).where(
@@ -874,7 +883,7 @@ async def verify_email_change(
raise HTTPException(status_code=404,
detail="No pending email change")
- if verif.expires_at.replace(tzinfo=timezone.utc) < now:
+ if verif.expires_at.replace(tzinfo=UTC) < now:
raise HTTPException(status_code=410, detail="Verification code expired")
if verif.attempts >= VERIFICATION_MAX_ATTEMPTS:
@@ -1090,7 +1099,7 @@ async def password_reset_request(
EmailVerification.user_id == user.id,
EmailVerification.purpose == "password_reset",
EmailVerification.created_at
- > datetime.now(timezone.utc) - timedelta(seconds=reset_cooldown),
+ > datetime.now(UTC) - timedelta(seconds=reset_cooldown),
))
if recent.first():
return {"status": "sent_if_exists"}
@@ -1110,7 +1119,7 @@ async def password_reset_request(
code=code,
purpose="password_reset",
user_id=user.id,
- expires_at=datetime.now(timezone.utc)
+ expires_at=datetime.now(UTC)
+ timedelta(seconds=PASSWORD_RESET_TTL),
))
db.add(IPLog(user_id=user.id, event="password_reset_request",
@@ -1141,7 +1150,7 @@ async def password_reset(
request: Request,
db: AsyncSession = Depends(get_db),
):
- now = datetime.now(timezone.utc)
+ now = datetime.now(UTC)
result = await db.execute(select(User).where(User.username == body.username))
user = result.scalar_one_or_none()
if not user:
@@ -1157,7 +1166,7 @@ async def password_reset(
if not verif:
raise HTTPException(status_code=404,
detail="No pending reset for this account")
- if verif.expires_at.replace(tzinfo=timezone.utc) < now:
+ if verif.expires_at.replace(tzinfo=UTC) < now:
raise HTTPException(status_code=410, detail="Reset code expired")
if verif.attempts >= VERIFICATION_MAX_ATTEMPTS:
raise HTTPException(status_code=429, detail="Too many attempts")
diff --git a/packages/meshbay-hub/src/meshbay_hub/app.py b/packages/meshbay-hub/src/meshbay_hub/app.py
index afc1cde..209dc32 100644
--- a/packages/meshbay-hub/src/meshbay_hub/app.py
+++ b/packages/meshbay-hub/src/meshbay_hub/app.py
@@ -11,37 +11,41 @@ Usage:
import asyncio
from contextlib import asynccontextmanager
-from pathlib import Path
from fastapi import FastAPI
from slowapi import _rate_limit_exceeded_handler
from slowapi.errors import RateLimitExceeded
from meshbay_hub import __version__
+from meshbay_hub.api.admin import router as admin_router
+from meshbay_hub.api.deps import set_admin_usernames
+from meshbay_hub.api.federation import router as federation_router
+from meshbay_hub.api.groups import router as groups_router
+from meshbay_hub.api.groups import swarm_router
+from meshbay_hub.api.health import router as health_router
+from meshbay_hub.api.hub import router as hub_router
+from meshbay_hub.api.hub import set_config as hub_set_config
+from meshbay_hub.api.middleware import limiter
+from meshbay_hub.api.moderation import router as moderation_router
+from meshbay_hub.api.nodes import router as nodes_router
+from meshbay_hub.api.notifications import router as notifications_router
+from meshbay_hub.api.relay import router as relay_router
+from meshbay_hub.api.revocation import router as revocation_router
+from meshbay_hub.api.signaling import router as signaling_router
+from meshbay_hub.api.users import router as users_router
+from meshbay_hub.api.users import set_config as users_set_config
+from meshbay_hub.api.webapp import ASSET_V, CSP, STATIC_DIR
+from meshbay_hub.api.webapp import router as webapp_router
from meshbay_hub.auth import generate_hub_keypair, load_hub_keypair
from meshbay_hub.config import HubConfig
+from meshbay_hub.csam import csam_router
from meshbay_hub.db.engine import close_db, init_db
-from meshbay_hub.api.hub import router as hub_router, set_config as hub_set_config
-from meshbay_hub.api.users import router as users_router, set_config as users_set_config
-from meshbay_hub.api.deps import set_admin_usernames
-from meshbay_hub.api.nodes import router as nodes_router
-from meshbay_hub.api.groups import router as groups_router, swarm_router
-from meshbay_hub.api.revocation import router as revocation_router
-from meshbay_hub.api.moderation import router as moderation_router
-from meshbay_hub.api.federation import router as federation_router
-from meshbay_hub.csam import csam_router
-from meshbay_hub.api.health import router as health_router
-from meshbay_hub.api.relay import router as relay_router
-from meshbay_hub.api.signaling import router as signaling_router
-from meshbay_hub.api.admin import router as admin_router
-from meshbay_hub.api.notifications import router as notifications_router
-from meshbay_hub.api.webapp import router as webapp_router, STATIC_DIR, ASSET_V, CSP
-from meshbay_hub.api.middleware import limiter
async def _sync_admin_roles(admin_usernames: list[str]) -> None:
"""Ensure config-listed admin usernames have role='admin' in the DB."""
- from sqlalchemy import select, update
+ from sqlalchemy import select
+
from meshbay_hub.db.engine import get_session_factory
from meshbay_hub.db.models import User
@@ -122,8 +126,8 @@ def create_app(cfg: HubConfig | None = None) -> FastAPI:
from meshbay_hub.csam import get_csam_checker
get_csam_checker().load()
- from meshbay_hub.tasks.cleanup import cleanup_loop
from meshbay_hub.db.engine import get_session_factory
+ from meshbay_hub.tasks.cleanup import cleanup_loop
cleanup_task = asyncio.create_task(cleanup_loop(get_session_factory()))
yield
diff --git a/packages/meshbay-hub/src/meshbay_hub/auth.py b/packages/meshbay-hub/src/meshbay_hub/auth.py
index 7045197..d038027 100644
--- a/packages/meshbay-hub/src/meshbay_hub/auth.py
+++ b/packages/meshbay-hub/src/meshbay_hub/auth.py
@@ -18,12 +18,12 @@ from pathlib import Path
import blake3
import jwt
-from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PrivateKey
from cryptography.hazmat.primitives import serialization
+from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PrivateKey
from cryptography.hazmat.primitives.ciphers.aead import AESGCM
+from cryptography.hazmat.primitives.hashes import SHA256
from cryptography.hazmat.primitives.kdf.argon2 import Argon2id
from cryptography.hazmat.primitives.kdf.hkdf import HKDF
-from cryptography.hazmat.primitives.hashes import SHA256
# Argon2id parameters — versioned for gradual migration
_ARGON2_LANES = 4
diff --git a/packages/meshbay-hub/src/meshbay_hub/csam.py b/packages/meshbay-hub/src/meshbay_hub/csam.py
index 2a0ce25..e540068 100644
--- a/packages/meshbay-hub/src/meshbay_hub/csam.py
+++ b/packages/meshbay-hub/src/meshbay_hub/csam.py
@@ -19,9 +19,7 @@ IMPORTANT: Never log matched hashes or file contents. CSAM detection
must be reported to NCMEC (US law) or relevant authority immediately.
"""
-import hashlib
import logging
-import os
from pathlib import Path
log = logging.getLogger(__name__)
@@ -123,7 +121,8 @@ def check_content_hash(blake3_hex: str) -> bool:
# ── Hub API integration ───────────────────────────────────────────────────────
-from fastapi import APIRouter, Depends, HTTPException, UploadFile, File
+from fastapi import APIRouter, Depends, HTTPException
+
from meshbay_hub.api.deps import require_admin
from meshbay_hub.db.models import User
diff --git a/packages/meshbay-hub/src/meshbay_hub/db/__init__.py b/packages/meshbay-hub/src/meshbay_hub/db/__init__.py
index 62e5388..9d7483a 100644
--- a/packages/meshbay-hub/src/meshbay_hub/db/__init__.py
+++ b/packages/meshbay-hub/src/meshbay_hub/db/__init__.py
@@ -1,6 +1,6 @@
"""Hub database layer."""
-from .engine import init_db, close_db, get_db
-from .models import Base, User, Node, Group, GroupMember, RefreshToken, IPLog
+from .engine import close_db, get_db, init_db
+from .models import Base, Group, GroupMember, IPLog, Node, RefreshToken, User
__all__ = [
"init_db", "close_db", "get_db",
diff --git a/packages/meshbay-hub/src/meshbay_hub/db/migrations/env.py b/packages/meshbay-hub/src/meshbay_hub/db/migrations/env.py
index 8908deb..61f80be 100644
--- a/packages/meshbay-hub/src/meshbay_hub/db/migrations/env.py
+++ b/packages/meshbay-hub/src/meshbay_hub/db/migrations/env.py
@@ -4,14 +4,12 @@ import asyncio
import os
from logging.config import fileConfig
+from alembic import context
+from meshbay_hub.db.models import Base
from sqlalchemy import pool
from sqlalchemy.engine import Connection
from sqlalchemy.ext.asyncio import async_engine_from_config
-from alembic import context
-
-from meshbay_hub.db.models import Base
-
config = context.config
if config.config_file_name is not None:
diff --git a/packages/meshbay-hub/src/meshbay_hub/db/migrations/versions/a7b8c9d0e1f2_add_group_last_activity.py b/packages/meshbay-hub/src/meshbay_hub/db/migrations/versions/a7b8c9d0e1f2_add_group_last_activity.py
index 8a4e2ee..034d12f 100644
--- a/packages/meshbay-hub/src/meshbay_hub/db/migrations/versions/a7b8c9d0e1f2_add_group_last_activity.py
+++ b/packages/meshbay-hub/src/meshbay_hub/db/migrations/versions/a7b8c9d0e1f2_add_group_last_activity.py
@@ -5,16 +5,15 @@ Revises: f1a2b3c4d5e6
Create Date: 2026-08-20 20:50:00.000000
"""
-from typing import Sequence, Union
+from collections.abc import Sequence
-from alembic import op
import sqlalchemy as sa
-
+from alembic import op
revision: str = 'a7b8c9d0e1f2'
-down_revision: Union[str, Sequence[str], None] = 'f1a2b3c4d5e6'
-branch_labels: Union[str, Sequence[str], None] = None
-depends_on: Union[str, Sequence[str], None] = None
+down_revision: str | Sequence[str] | None = 'f1a2b3c4d5e6'
+branch_labels: str | Sequence[str] | None = None
+depends_on: str | Sequence[str] | None = None
def upgrade() -> None:
diff --git a/packages/meshbay-hub/src/meshbay_hub/db/migrations/versions/a9b8c7d6e5f4_add_login_throttle.py b/packages/meshbay-hub/src/meshbay_hub/db/migrations/versions/a9b8c7d6e5f4_add_login_throttle.py
index 2fead6c..45985e2 100644
--- a/packages/meshbay-hub/src/meshbay_hub/db/migrations/versions/a9b8c7d6e5f4_add_login_throttle.py
+++ b/packages/meshbay-hub/src/meshbay_hub/db/migrations/versions/a9b8c7d6e5f4_add_login_throttle.py
@@ -7,15 +7,15 @@ Revision ID: a9b8c7d6e5f4
Revises: e5f6a7b8c9d0
"""
-from typing import Sequence, Union
+from collections.abc import Sequence
import sqlalchemy as sa
from alembic import op
revision: str = "a9b8c7d6e5f4"
-down_revision: Union[str, Sequence[str], None] = "e5f6a7b8c9d0"
-branch_labels: Union[str, Sequence[str], None] = None
-depends_on: Union[str, Sequence[str], None] = None
+down_revision: str | Sequence[str] | None = "e5f6a7b8c9d0"
+branch_labels: str | Sequence[str] | None = None
+depends_on: str | Sequence[str] | None = None
def upgrade() -> None:
diff --git a/packages/meshbay-hub/src/meshbay_hub/db/migrations/versions/b1c2d3e4f5a6_add_hub_settings.py b/packages/meshbay-hub/src/meshbay_hub/db/migrations/versions/b1c2d3e4f5a6_add_hub_settings.py
index 13f2b5c..9a90f7a 100644
--- a/packages/meshbay-hub/src/meshbay_hub/db/migrations/versions/b1c2d3e4f5a6_add_hub_settings.py
+++ b/packages/meshbay-hub/src/meshbay_hub/db/migrations/versions/b1c2d3e4f5a6_add_hub_settings.py
@@ -5,16 +5,15 @@ Revises: a7b8c9d0e1f2
Create Date: 2026-08-28 12:00:00.000000
"""
-from typing import Sequence, Union
+from collections.abc import Sequence
-from alembic import op
import sqlalchemy as sa
-
+from alembic import op
revision: str = 'b1c2d3e4f5a6'
-down_revision: Union[str, Sequence[str], None] = 'a7b8c9d0e1f2'
-branch_labels: Union[str, Sequence[str], None] = None
-depends_on: Union[str, Sequence[str], None] = None
+down_revision: str | Sequence[str] | None = 'a7b8c9d0e1f2'
+branch_labels: str | Sequence[str] | None = None
+depends_on: str | Sequence[str] | None = None
def upgrade() -> None:
diff --git a/packages/meshbay-hub/src/meshbay_hub/db/migrations/versions/c3d4e5f6a7b8_group_name_unique_per_owner.py b/packages/meshbay-hub/src/meshbay_hub/db/migrations/versions/c3d4e5f6a7b8_group_name_unique_per_owner.py
index 6fc5b73..fea0583 100644
--- a/packages/meshbay-hub/src/meshbay_hub/db/migrations/versions/c3d4e5f6a7b8_group_name_unique_per_owner.py
+++ b/packages/meshbay-hub/src/meshbay_hub/db/migrations/versions/c3d4e5f6a7b8_group_name_unique_per_owner.py
@@ -10,16 +10,15 @@ its UUID — this only makes `name@owner` a dependable handle.
Pre-flight: abort if the data already violates it, with the offending
(admin_id, name) pairs listed, rather than silently renaming anyone's group.
"""
-from typing import Sequence, Union
+from collections.abc import Sequence
-from alembic import op
import sqlalchemy as sa
-
+from alembic import op
revision: str = 'c3d4e5f6a7b8'
-down_revision: Union[str, Sequence[str], None] = 'b1c2d3e4f5a6'
-branch_labels: Union[str, Sequence[str], None] = None
-depends_on: Union[str, Sequence[str], None] = None
+down_revision: str | Sequence[str] | None = 'b1c2d3e4f5a6'
+branch_labels: str | Sequence[str] | None = None
+depends_on: str | Sequence[str] | None = None
def upgrade() -> None:
diff --git a/packages/meshbay-hub/src/meshbay_hub/db/migrations/versions/d28b9caf9f07_initial_schema.py b/packages/meshbay-hub/src/meshbay_hub/db/migrations/versions/d28b9caf9f07_initial_schema.py
index 6301426..a814f37 100644
--- a/packages/meshbay-hub/src/meshbay_hub/db/migrations/versions/d28b9caf9f07_initial_schema.py
+++ b/packages/meshbay-hub/src/meshbay_hub/db/migrations/versions/d28b9caf9f07_initial_schema.py
@@ -12,16 +12,15 @@ Revises:
Create Date: 2026-08-09 04:35:07.120021
"""
-from typing import Sequence, Union
+from collections.abc import Sequence
-from alembic import op
import sqlalchemy as sa
-
+from alembic import op
revision: str = 'd28b9caf9f07'
-down_revision: Union[str, Sequence[str], None] = None
-branch_labels: Union[str, Sequence[str], None] = None
-depends_on: Union[str, Sequence[str], None] = None
+down_revision: str | Sequence[str] | None = None
+branch_labels: str | Sequence[str] | None = None
+depends_on: str | Sequence[str] | None = None
def upgrade() -> None:
diff --git a/packages/meshbay-hub/src/meshbay_hub/db/migrations/versions/d4e5f6a7b8c9_add_email_verification.py b/packages/meshbay-hub/src/meshbay_hub/db/migrations/versions/d4e5f6a7b8c9_add_email_verification.py
index 6741eb8..aa15b6d 100644
--- a/packages/meshbay-hub/src/meshbay_hub/db/migrations/versions/d4e5f6a7b8c9_add_email_verification.py
+++ b/packages/meshbay-hub/src/meshbay_hub/db/migrations/versions/d4e5f6a7b8c9_add_email_verification.py
@@ -9,16 +9,15 @@ Revises: c3d4e5f6a7b8
Create Date: 2026-08-31 14:00:00.000000
"""
-from typing import Sequence, Union
+from collections.abc import Sequence
-from alembic import op
import sqlalchemy as sa
-
+from alembic import op
revision: str = 'd4e5f6a7b8c9'
-down_revision: Union[str, Sequence[str], None] = 'c3d4e5f6a7b8'
-branch_labels: Union[str, Sequence[str], None] = None
-depends_on: Union[str, Sequence[str], None] = None
+down_revision: str | Sequence[str] | None = 'c3d4e5f6a7b8'
+branch_labels: str | Sequence[str] | None = None
+depends_on: str | Sequence[str] | None = None
def upgrade() -> None:
diff --git a/packages/meshbay-hub/src/meshbay_hub/db/migrations/versions/e5f6a7b8c9d0_add_mail_quota.py b/packages/meshbay-hub/src/meshbay_hub/db/migrations/versions/e5f6a7b8c9d0_add_mail_quota.py
index 8f2e4d2..c729de4 100644
--- a/packages/meshbay-hub/src/meshbay_hub/db/migrations/versions/e5f6a7b8c9d0_add_mail_quota.py
+++ b/packages/meshbay-hub/src/meshbay_hub/db/migrations/versions/e5f6a7b8c9d0_add_mail_quota.py
@@ -8,15 +8,15 @@ Revision ID: e5f6a7b8c9d0
Revises: d4e5f6a7b8c9
"""
-from typing import Sequence, Union
+from collections.abc import Sequence
import sqlalchemy as sa
from alembic import op
revision: str = "e5f6a7b8c9d0"
-down_revision: Union[str, Sequence[str], None] = "d4e5f6a7b8c9"
-branch_labels: Union[str, Sequence[str], None] = None
-depends_on: Union[str, Sequence[str], None] = None
+down_revision: str | Sequence[str] | None = "d4e5f6a7b8c9"
+branch_labels: str | Sequence[str] | None = None
+depends_on: str | Sequence[str] | None = None
def upgrade() -> None:
diff --git a/packages/meshbay-hub/src/meshbay_hub/db/migrations/versions/f1a2b3c4d5e6_add_user_preferences.py b/packages/meshbay-hub/src/meshbay_hub/db/migrations/versions/f1a2b3c4d5e6_add_user_preferences.py
index e30a4da..9f8e729 100644
--- a/packages/meshbay-hub/src/meshbay_hub/db/migrations/versions/f1a2b3c4d5e6_add_user_preferences.py
+++ b/packages/meshbay-hub/src/meshbay_hub/db/migrations/versions/f1a2b3c4d5e6_add_user_preferences.py
@@ -5,16 +5,15 @@ Revises: d28b9caf9f07
Create Date: 2026-08-19 12:00:00.000000
"""
-from typing import Sequence, Union
+from collections.abc import Sequence
-from alembic import op
import sqlalchemy as sa
-
+from alembic import op
revision: str = 'f1a2b3c4d5e6'
-down_revision: Union[str, Sequence[str], None] = 'd28b9caf9f07'
-branch_labels: Union[str, Sequence[str], None] = None
-depends_on: Union[str, Sequence[str], None] = None
+down_revision: str | Sequence[str] | None = 'd28b9caf9f07'
+branch_labels: str | Sequence[str] | None = None
+depends_on: str | Sequence[str] | None = None
def upgrade() -> None:
diff --git a/packages/meshbay-hub/src/meshbay_hub/db/models.py b/packages/meshbay-hub/src/meshbay_hub/db/models.py
index 62e4a11..38c4723 100644
--- a/packages/meshbay-hub/src/meshbay_hub/db/models.py
+++ b/packages/meshbay-hub/src/meshbay_hub/db/models.py
@@ -11,17 +11,23 @@ Tables:
"""
import uuid
-from datetime import datetime, timezone
+from datetime import UTC, datetime
from sqlalchemy import (
- Boolean, DateTime, ForeignKey, Index, Integer,
- String, Text, UniqueConstraint, text,
+ Boolean,
+ DateTime,
+ ForeignKey,
+ Index,
+ Integer,
+ String,
+ Text,
+ text,
)
from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column, relationship
def _now() -> datetime:
- return datetime.now(timezone.utc)
+ return datetime.now(UTC)
def _uuid() -> str:
return str(uuid.uuid4())
diff --git a/packages/meshbay-hub/src/meshbay_hub/login_throttle.py b/packages/meshbay-hub/src/meshbay_hub/login_throttle.py
index 3281088..bfd142e 100644
--- a/packages/meshbay-hub/src/meshbay_hub/login_throttle.py
+++ b/packages/meshbay-hub/src/meshbay_hub/login_throttle.py
@@ -24,7 +24,7 @@ locks somebody else's name from signing them out (§13.5b, AV26).
"""
import hashlib
-from datetime import datetime, timedelta, timezone
+from datetime import UTC, datetime, timedelta
from sqlalchemy import case, delete, select, update
from sqlalchemy.ext.asyncio import AsyncSession
@@ -39,7 +39,7 @@ def _key(username: str) -> str:
def _aware(dt: datetime) -> datetime:
# SQLite hands back naive datetimes for a timezone-aware column.
- return dt if dt.tzinfo is not None else dt.replace(tzinfo=timezone.utc)
+ return dt if dt.tzinfo is not None else dt.replace(tzinfo=UTC)
def _insert_for(db: AsyncSession):
@@ -64,7 +64,7 @@ async def reserve(db: AsyncSession, username: str) -> tuple[bool, int]:
if max_failures == 0:
return True, 0
- now = datetime.now(timezone.utc)
+ now = datetime.now(UTC)
window = timedelta(minutes=limits["lockout_minutes"])
window_start = now - window
key = _key(username)
@@ -102,7 +102,7 @@ async def locked_for(db: AsyncSession, username: str) -> int:
return 0
remaining = (_aware(row.last_failure_at)
+ timedelta(minutes=limits["lockout_minutes"])
- - datetime.now(timezone.utc)).total_seconds()
+ - datetime.now(UTC)).total_seconds()
return max(0, int(remaining + 0.999))
@@ -136,7 +136,7 @@ async def clear(db: AsyncSession, username: str) -> None:
async def purge_expired(db: AsyncSession) -> int:
"""Rows whose failures have aged out. Every unknown name typed creates one."""
limits = await hub_settings.login_limits(db)
- cutoff = datetime.now(timezone.utc) - timedelta(minutes=limits["lockout_minutes"])
+ cutoff = datetime.now(UTC) - timedelta(minutes=limits["lockout_minutes"])
result = await db.execute(
delete(LoginThrottle).where(LoginThrottle.last_failure_at < cutoff))
await db.commit()
diff --git a/packages/meshbay-hub/src/meshbay_hub/mail.py b/packages/meshbay-hub/src/meshbay_hub/mail.py
index 126ed45..1eb7c51 100644
--- a/packages/meshbay-hub/src/meshbay_hub/mail.py
+++ b/packages/meshbay-hub/src/meshbay_hub/mail.py
@@ -16,7 +16,7 @@ import asyncio
import hashlib
import logging
import smtplib
-from datetime import datetime, timedelta, timezone
+from datetime import UTC, datetime, timedelta
from email.message import EmailMessage
log = logging.getLogger(__name__)
@@ -79,7 +79,7 @@ async def _take(db, key: str, window: timedelta, ceiling: int,
"""
from meshbay_hub.db.models import MailQuota
- now = datetime.now(timezone.utc)
+ now = datetime.now(UTC)
row = await db.get(MailQuota, key)
if row is None:
row = MailQuota(key=key, window_start=now, count=0, last_sent=None)
@@ -87,14 +87,14 @@ async def _take(db, key: str, window: timedelta, ceiling: int,
started = row.window_start
if started.tzinfo is None:
- started = started.replace(tzinfo=timezone.utc)
+ started = started.replace(tzinfo=UTC)
if now - started >= window:
row.window_start, row.count = now, 0
if cooldown is not None and row.last_sent is not None:
last = row.last_sent
if last.tzinfo is None:
- last = last.replace(tzinfo=timezone.utc)
+ last = last.replace(tzinfo=UTC)
if now - last < cooldown:
raise MailRefused("too soon since the last message to this recipient")
@@ -131,12 +131,12 @@ async def _announce_exhaustion(scope: str) -> None:
try:
async with get_session_factory()() as db:
key = f"alert:{scope}"
- now = datetime.now(timezone.utc)
+ now = datetime.now(UTC)
row = await db.get(MailQuota, key)
if row is not None and row.last_sent is not None:
last = row.last_sent
if last.tzinfo is None:
- last = last.replace(tzinfo=timezone.utc)
+ last = last.replace(tzinfo=UTC)
if now - last < timedelta(hours=1):
return
if row is None:
@@ -202,9 +202,10 @@ async def reserve(db, purpose: str, address: str) -> None:
async def status(db) -> dict:
"""What the operator sees in the panel: is the hub still sending?"""
+ from sqlalchemy import func, select
+
from meshbay_hub import hub_settings
from meshbay_hub.db.models import MailQuota
- from sqlalchemy import func, select
limits = await hub_settings.mail_limits(db)
row = await db.get(MailQuota, "hour")
@@ -213,8 +214,8 @@ async def status(db) -> dict:
if row is not None:
started = row.window_start
if started.tzinfo is None:
- started = started.replace(tzinfo=timezone.utc)
- if datetime.now(timezone.utc) - started < timedelta(hours=1):
+ started = started.replace(tzinfo=UTC)
+ if datetime.now(UTC) - started < timedelta(hours=1):
used, window_start = row.count, started.isoformat()
recipients = await db.scalar(
@@ -244,10 +245,11 @@ async def status(db) -> dict:
async def purge_expired_quota(db) -> int:
"""Drop counters whose window has passed. Returns how many went."""
- from meshbay_hub.db.models import MailQuota
from sqlalchemy import delete
- cutoff = datetime.now(timezone.utc) - timedelta(days=1)
+ from meshbay_hub.db.models import MailQuota
+
+ cutoff = datetime.now(UTC) - timedelta(days=1)
result = await db.execute(
delete(MailQuota).where(MailQuota.window_start < cutoff))
await db.commit()
diff --git a/packages/meshbay-hub/src/meshbay_hub/tasks/cleanup.py b/packages/meshbay-hub/src/meshbay_hub/tasks/cleanup.py
index 5c52387..429575f 100644
--- a/packages/meshbay-hub/src/meshbay_hub/tasks/cleanup.py
+++ b/packages/meshbay-hub/src/meshbay_hub/tasks/cleanup.py
@@ -2,7 +2,7 @@
import asyncio
import logging
-from datetime import datetime, timedelta, timezone
+from datetime import UTC, datetime, timedelta
from sqlalchemy import delete, select
from sqlalchemy.ext.asyncio import AsyncSession
@@ -16,7 +16,7 @@ CLEANUP_INTERVAL_HOURS = 24
async def purge_old_ip_logs(db: AsyncSession, retention_days: int = RETENTION_DAYS) -> int:
- cutoff = datetime.now(timezone.utc) - timedelta(days=retention_days)
+ cutoff = datetime.now(UTC) - timedelta(days=retention_days)
result = await db.execute(delete(IPLog).where(IPLog.timestamp < cutoff))
await db.commit()
return result.rowcount
@@ -26,7 +26,7 @@ PENDING_USER_EXPIRY_DAYS = 7
async def purge_expired_verifications(db: AsyncSession) -> int:
- now = datetime.now(timezone.utc)
+ now = datetime.now(UTC)
result = await db.execute(
delete(EmailVerification).where(EmailVerification.expires_at < now))
await db.commit()
@@ -35,7 +35,7 @@ async def purge_expired_verifications(db: AsyncSession) -> int:
async def purge_stale_pending_users(db: AsyncSession,
expiry_days: int = PENDING_USER_EXPIRY_DAYS) -> int:
- cutoff = datetime.now(timezone.utc) - timedelta(days=expiry_days)
+ cutoff = datetime.now(UTC) - timedelta(days=expiry_days)
result = await db.execute(
delete(User).where(User.status == "pending", User.created_at < cutoff))
await db.commit()
@@ -93,7 +93,7 @@ async def find_unhosted_groups(db: AsyncSession, grace_days: int = UNHOSTED_GRAC
whole reason the column exists rather than a check against the live socket
registry, which would delete every group during a hub restart.
"""
- cutoff = datetime.now(timezone.utc) - timedelta(days=grace_days)
+ cutoff = datetime.now(UTC) - timedelta(days=grace_days)
result = await db.execute(
select(Group).where(Group.hosted_at.is_(None), Group.created_at < cutoff))
return list(result.scalars().all())