refactor(gateway): rename persistence models to external chat

This commit is contained in:
Anish Sarkar 2026-05-28 04:37:27 +05:30
parent f2d82234d4
commit a57b741d5e
8 changed files with 274 additions and 323 deletions

View file

@ -1,4 +1,4 @@
"""Gateway account helpers."""
"""External chat account helpers."""
from __future__ import annotations
@ -7,16 +7,16 @@ from sqlalchemy.ext.asyncio import AsyncSession
from app.config import config
from app.db import (
GatewayAccountMode,
GatewayHealthStatus,
GatewayPlatform,
GatewayPlatformAccount,
ExternalChatAccountMode,
ExternalChatHealthStatus,
ExternalChatPlatform,
ExternalChatAccount,
)
from app.utils.oauth_security import TokenEncryption
def account_token(account: GatewayPlatformAccount) -> str | None:
if account.is_system_account and account.platform == GatewayPlatform.TELEGRAM:
def account_token(account: ExternalChatAccount) -> str | None:
if account.is_system_account and account.platform == ExternalChatPlatform.TELEGRAM:
return config.TELEGRAM_SHARED_BOT_TOKEN
if not account.encrypted_credentials:
return None
@ -27,26 +27,24 @@ def account_token(account: GatewayPlatformAccount) -> str | None:
async def get_or_create_system_telegram_account(
session: AsyncSession,
) -> GatewayPlatformAccount:
) -> ExternalChatAccount:
result = await session.execute(
select(GatewayPlatformAccount).where(
GatewayPlatformAccount.platform == GatewayPlatform.TELEGRAM,
GatewayPlatformAccount.is_system_account.is_(True),
select(ExternalChatAccount).where(
ExternalChatAccount.platform == ExternalChatPlatform.TELEGRAM,
ExternalChatAccount.is_system_account.is_(True),
)
)
account = result.scalars().first()
if account is not None:
return account
account = GatewayPlatformAccount(
platform=GatewayPlatform.TELEGRAM,
mode=GatewayAccountMode.CLOUD_SHARED,
account = ExternalChatAccount(
platform=ExternalChatPlatform.TELEGRAM,
mode=ExternalChatAccountMode.CLOUD_SHARED,
is_system_account=True,
account_metadata={
"bot_username": config.TELEGRAM_SHARED_BOT_USERNAME,
"webhook_secret": config.TELEGRAM_WEBHOOK_SECRET,
},
bot_username=config.TELEGRAM_SHARED_BOT_USERNAME,
webhook_secret=config.TELEGRAM_WEBHOOK_SECRET,
cursor_state={},
health_status=GatewayHealthStatus.UNKNOWN,
health_status=ExternalChatHealthStatus.UNKNOWN,
)
session.add(account)
await session.flush()

View file

@ -1,11 +1,11 @@
"""Authorization invariants for gateway-routed turns."""
"""Authorization invariants for external-chat-routed turns."""
from __future__ import annotations
from fastapi import HTTPException
from sqlalchemy.ext.asyncio import AsyncSession
from app.db import GatewayConversationBinding, Permission, User
from app.db import ExternalChatBinding, Permission, User
from app.gateway.bindings import suspend_binding
from app.observability.metrics import record_gateway_auth_invariant_failure
from app.utils.rbac import check_permission, check_search_space_access
@ -19,7 +19,7 @@ class GatewaySuspendedError(RuntimeError):
async def _fail(
session: AsyncSession,
binding: GatewayConversationBinding,
binding: ExternalChatBinding,
reason: str,
) -> None:
suspend_binding(binding, reason)
@ -30,7 +30,7 @@ async def _fail(
async def assert_authorization_invariant(
session: AsyncSession,
binding: GatewayConversationBinding,
binding: ExternalChatBinding,
) -> User:
if binding.state != "bound":
await _fail(session, binding, "binding_not_bound")
@ -46,7 +46,7 @@ async def assert_authorization_invariant(
user,
binding.search_space_id,
Permission.CHATS_CREATE.value,
"Gateway owner no longer has permission to chat in this search space",
"External chat owner no longer has permission to chat in this search space",
)
except HTTPException as exc:
await _fail(session, binding, f"rbac_{exc.status_code}")

View file

@ -1,4 +1,4 @@
"""Gateway binding helpers."""
"""External chat binding helpers."""
from __future__ import annotations
@ -9,19 +9,19 @@ from sqlalchemy.ext.asyncio import AsyncSession
from app.db import (
ChatVisibility,
GatewayBindingState,
GatewayConversationBinding,
ExternalChatBindingState,
ExternalChatBinding,
NewChatThread,
)
async def get_or_create_thread_for_binding(
session: AsyncSession,
binding: GatewayConversationBinding,
binding: ExternalChatBinding,
) -> NewChatThread:
if binding.active_thread_id is not None:
if binding.new_chat_thread_id is not None:
result = await session.execute(
select(NewChatThread).where(NewChatThread.id == binding.active_thread_id)
select(NewChatThread).where(NewChatThread.id == binding.new_chat_thread_id)
)
thread = result.scalars().first()
if thread is not None and not thread.archived:
@ -33,30 +33,30 @@ async def get_or_create_thread_for_binding(
created_by_id=binding.user_id,
visibility=ChatVisibility.PRIVATE,
source="telegram",
binding_id=binding.id,
external_chat_binding_id=binding.id,
)
session.add(thread)
await session.flush()
binding.active_thread_id = thread.id
binding.new_chat_thread_id = thread.id
return thread
def suspend_binding(binding: GatewayConversationBinding, reason: str) -> None:
def suspend_binding(binding: ExternalChatBinding, reason: str) -> None:
now = datetime.now(UTC)
binding.state = GatewayBindingState.SUSPENDED
binding.state = ExternalChatBindingState.SUSPENDED
binding.suspended_at = now
binding.suspended_reason = reason
def revoke_binding(binding: GatewayConversationBinding) -> None:
def revoke_binding(binding: ExternalChatBinding) -> None:
now = datetime.now(UTC)
binding.state = GatewayBindingState.REVOKED
binding.state = ExternalChatBindingState.REVOKED
binding.revoked_at = now
binding.active_thread_id = None
binding.new_chat_thread_id = None
def resume_binding(binding: GatewayConversationBinding) -> None:
binding.state = GatewayBindingState.BOUND
def resume_binding(binding: ExternalChatBinding) -> None:
binding.state = ExternalChatBindingState.BOUND
binding.suspended_at = None
binding.suspended_reason = None

View file

@ -5,7 +5,7 @@ from __future__ import annotations
from sqlalchemy.dialects.postgresql import insert
from sqlalchemy.ext.asyncio import AsyncSession
from app.db import GatewayInboundEvent, GatewayPlatform
from app.db import ExternalChatInboundEvent, ExternalChatPlatform
def telegram_event_dedupe_key(update_id: int | str) -> str:
@ -16,15 +16,16 @@ async def persist_inbound_event(
session: AsyncSession,
*,
account_id: int,
platform: GatewayPlatform,
platform: ExternalChatPlatform,
event_dedupe_key: str,
event_kind: str,
raw_payload: dict,
external_event_id: str | None = None,
external_message_id: str | None = None,
request_id: str | None = None,
) -> int | None:
stmt = (
insert(GatewayInboundEvent)
insert(ExternalChatInboundEvent)
.values(
account_id=account_id,
platform=platform,
@ -33,11 +34,12 @@ async def persist_inbound_event(
external_message_id=external_message_id,
event_kind=event_kind,
raw_payload=raw_payload,
request_id=request_id,
)
.on_conflict_do_nothing(
index_elements=["account_id", "event_dedupe_key"],
)
.returning(GatewayInboundEvent.id)
.returning(ExternalChatInboundEvent.id)
)
result = await session.execute(stmt)
return result.scalar_one_or_none()

View file

@ -1,4 +1,4 @@
"""Pairing code lifecycle for gateway bindings."""
"""Pairing code lifecycle for external chat bindings."""
from __future__ import annotations
@ -8,7 +8,7 @@ from datetime import UTC, datetime, timedelta
from sqlalchemy import select
from sqlalchemy.ext.asyncio import AsyncSession
from app.db import GatewayBindingState, GatewayConversationBinding
from app.db import ExternalChatBindingState, ExternalChatBinding
PAIRING_CODE_TTL = timedelta(minutes=10)
@ -30,19 +30,19 @@ async def redeem_pairing_code(
external_display_name: str | None,
external_username: str | None,
external_metadata: dict | None = None,
) -> GatewayConversationBinding | None:
) -> ExternalChatBinding | None:
result = await session.execute(
select(GatewayConversationBinding).where(
GatewayConversationBinding.pairing_code == code,
GatewayConversationBinding.state == GatewayBindingState.PENDING,
GatewayConversationBinding.pairing_code_expires_at > datetime.now(UTC),
select(ExternalChatBinding).where(
ExternalChatBinding.pairing_code == code,
ExternalChatBinding.state == ExternalChatBindingState.PENDING,
ExternalChatBinding.pairing_code_expires_at > datetime.now(UTC),
)
)
binding = result.scalars().first()
if binding is None:
return None
binding.state = GatewayBindingState.BOUND
binding.state = ExternalChatBindingState.BOUND
binding.pairing_code = None
binding.pairing_code_expires_at = None
binding.external_peer_id = external_peer_id