mirror of
https://github.com/MODSetter/SurfSense.git
synced 2026-06-04 20:05:16 +02:00
feat(gateway): improve WhatsApp account mode handling and connection filtering
This commit is contained in:
parent
a151e8f729
commit
fc2467be3d
3 changed files with 82 additions and 12 deletions
|
|
@ -16,6 +16,7 @@ from sqlalchemy.ext.asyncio import AsyncSession, async_sessionmaker
|
||||||
from app.config import config
|
from app.config import config
|
||||||
from app.db import (
|
from app.db import (
|
||||||
ExternalChatAccount,
|
ExternalChatAccount,
|
||||||
|
ExternalChatAccountMode,
|
||||||
ExternalChatBinding,
|
ExternalChatBinding,
|
||||||
ExternalChatBindingState,
|
ExternalChatBindingState,
|
||||||
ExternalChatEventStatus,
|
ExternalChatEventStatus,
|
||||||
|
|
@ -40,6 +41,21 @@ def _dashboard_url() -> str:
|
||||||
return config.NEXT_FRONTEND_URL or "/dashboard"
|
return config.NEXT_FRONTEND_URL or "/dashboard"
|
||||||
|
|
||||||
|
|
||||||
|
def _active_whatsapp_account_mode() -> ExternalChatAccountMode | None:
|
||||||
|
if config.GATEWAY_WHATSAPP_INTAKE_MODE == "cloud":
|
||||||
|
return ExternalChatAccountMode.CLOUD_SHARED
|
||||||
|
if config.GATEWAY_WHATSAPP_INTAKE_MODE == "baileys":
|
||||||
|
return ExternalChatAccountMode.SELF_HOST_BYO
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def _is_inactive_whatsapp_account(account: ExternalChatAccount) -> bool:
|
||||||
|
return (
|
||||||
|
account.platform == ExternalChatPlatform.WHATSAPP
|
||||||
|
and account.mode != _active_whatsapp_account_mode()
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
async def claim_next_inbound_event(
|
async def claim_next_inbound_event(
|
||||||
session_maker: SessionMaker = async_session_maker,
|
session_maker: SessionMaker = async_session_maker,
|
||||||
) -> int | None:
|
) -> int | None:
|
||||||
|
|
@ -293,6 +309,11 @@ async def _dispatch_inbound_event(
|
||||||
event.last_error = "account_missing"
|
event.last_error = "account_missing"
|
||||||
await session.commit()
|
await session.commit()
|
||||||
return
|
return
|
||||||
|
if _is_inactive_whatsapp_account(account):
|
||||||
|
event.status = ExternalChatEventStatus.IGNORED
|
||||||
|
event.last_error = "inactive_whatsapp_mode"
|
||||||
|
await session.commit()
|
||||||
|
return
|
||||||
|
|
||||||
try:
|
try:
|
||||||
bundle = resolve_platform_bundle(account)
|
bundle = resolve_platform_bundle(account)
|
||||||
|
|
|
||||||
|
|
@ -16,7 +16,7 @@ from uuid import UUID
|
||||||
import httpx
|
import httpx
|
||||||
from fastapi import APIRouter, Depends, HTTPException, Request
|
from fastapi import APIRouter, Depends, HTTPException, Request
|
||||||
from pydantic import BaseModel
|
from pydantic import BaseModel
|
||||||
from sqlalchemy import select
|
from sqlalchemy import or_, select
|
||||||
from sqlalchemy.ext.asyncio import AsyncSession
|
from sqlalchemy.ext.asyncio import AsyncSession
|
||||||
from starlette.responses import JSONResponse, RedirectResponse, Response
|
from starlette.responses import JSONResponse, RedirectResponse, Response
|
||||||
|
|
||||||
|
|
@ -173,6 +173,21 @@ class UpdateAccountSearchSpaceRequest(BaseModel):
|
||||||
search_space_id: int
|
search_space_id: int
|
||||||
|
|
||||||
|
|
||||||
|
def _active_whatsapp_account_mode() -> ExternalChatAccountMode | None:
|
||||||
|
if config.GATEWAY_WHATSAPP_INTAKE_MODE == "cloud":
|
||||||
|
return ExternalChatAccountMode.CLOUD_SHARED
|
||||||
|
if config.GATEWAY_WHATSAPP_INTAKE_MODE == "baileys":
|
||||||
|
return ExternalChatAccountMode.SELF_HOST_BYO
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def _is_inactive_whatsapp_account(account: ExternalChatAccount) -> bool:
|
||||||
|
return (
|
||||||
|
account.platform == ExternalChatPlatform.WHATSAPP
|
||||||
|
and account.mode != _active_whatsapp_account_mode()
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def _classify_telegram_event(payload: dict[str, Any]) -> str:
|
def _classify_telegram_event(payload: dict[str, Any]) -> str:
|
||||||
if "message" in payload:
|
if "message" in payload:
|
||||||
return "message"
|
return "message"
|
||||||
|
|
@ -712,6 +727,10 @@ async def list_connections(
|
||||||
user: User = Depends(current_active_user),
|
user: User = Depends(current_active_user),
|
||||||
session: AsyncSession = Depends(get_async_session),
|
session: AsyncSession = Depends(get_async_session),
|
||||||
) -> list[dict[str, Any]]:
|
) -> list[dict[str, Any]]:
|
||||||
|
active_whatsapp_mode = _active_whatsapp_account_mode()
|
||||||
|
if platform == ExternalChatPlatform.WHATSAPP and active_whatsapp_mode is None:
|
||||||
|
return []
|
||||||
|
|
||||||
filters = [
|
filters = [
|
||||||
ExternalChatBinding.user_id == user.id,
|
ExternalChatBinding.user_id == user.id,
|
||||||
ExternalChatBinding.state.in_(
|
ExternalChatBinding.state.in_(
|
||||||
|
|
@ -720,6 +739,17 @@ async def list_connections(
|
||||||
]
|
]
|
||||||
if platform is not None:
|
if platform is not None:
|
||||||
filters.append(ExternalChatAccount.platform == platform)
|
filters.append(ExternalChatAccount.platform == platform)
|
||||||
|
if platform == ExternalChatPlatform.WHATSAPP and active_whatsapp_mode is not None:
|
||||||
|
filters.append(ExternalChatAccount.mode == active_whatsapp_mode)
|
||||||
|
elif active_whatsapp_mode is None:
|
||||||
|
filters.append(ExternalChatAccount.platform != ExternalChatPlatform.WHATSAPP)
|
||||||
|
else:
|
||||||
|
filters.append(
|
||||||
|
or_(
|
||||||
|
ExternalChatAccount.platform != ExternalChatPlatform.WHATSAPP,
|
||||||
|
ExternalChatAccount.mode == active_whatsapp_mode,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
result = await session.execute(
|
result = await session.execute(
|
||||||
select(ExternalChatBinding, ExternalChatAccount)
|
select(ExternalChatBinding, ExternalChatAccount)
|
||||||
|
|
@ -782,7 +812,10 @@ async def list_connections(
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
if platform is None or platform == ExternalChatPlatform.WHATSAPP:
|
if (
|
||||||
|
active_whatsapp_mode == ExternalChatAccountMode.SELF_HOST_BYO
|
||||||
|
and (platform is None or platform == ExternalChatPlatform.WHATSAPP)
|
||||||
|
):
|
||||||
account_result = await session.execute(
|
account_result = await session.execute(
|
||||||
select(ExternalChatAccount).where(
|
select(ExternalChatAccount).where(
|
||||||
ExternalChatAccount.owner_user_id == user.id,
|
ExternalChatAccount.owner_user_id == user.id,
|
||||||
|
|
@ -855,6 +888,9 @@ async def update_binding_search_space(
|
||||||
ExternalChatBindingState.SUSPENDED,
|
ExternalChatBindingState.SUSPENDED,
|
||||||
}:
|
}:
|
||||||
raise HTTPException(status_code=400, detail="Only active bindings can be routed")
|
raise HTTPException(status_code=400, detail="Only active bindings can be routed")
|
||||||
|
account = await session.get(ExternalChatAccount, binding.account_id)
|
||||||
|
if account is None or _is_inactive_whatsapp_account(account):
|
||||||
|
raise HTTPException(status_code=404, detail="Binding not found")
|
||||||
|
|
||||||
await check_search_space_access(session, user, body.search_space_id)
|
await check_search_space_access(session, user, body.search_space_id)
|
||||||
if binding.search_space_id != body.search_space_id:
|
if binding.search_space_id != body.search_space_id:
|
||||||
|
|
@ -878,6 +914,7 @@ async def update_gateway_account_search_space(
|
||||||
or account.owner_user_id != user.id
|
or account.owner_user_id != user.id
|
||||||
or account.platform != ExternalChatPlatform.WHATSAPP
|
or account.platform != ExternalChatPlatform.WHATSAPP
|
||||||
or account.mode != ExternalChatAccountMode.SELF_HOST_BYO
|
or account.mode != ExternalChatAccountMode.SELF_HOST_BYO
|
||||||
|
or _is_inactive_whatsapp_account(account)
|
||||||
):
|
):
|
||||||
raise HTTPException(status_code=404, detail="Gateway account not found")
|
raise HTTPException(status_code=404, detail="Gateway account not found")
|
||||||
|
|
||||||
|
|
@ -912,6 +949,9 @@ async def delete_binding(
|
||||||
binding = await session.get(ExternalChatBinding, binding_id)
|
binding = await session.get(ExternalChatBinding, binding_id)
|
||||||
if binding is None or binding.user_id != user.id:
|
if binding is None or binding.user_id != user.id:
|
||||||
raise HTTPException(status_code=404, detail="Binding not found")
|
raise HTTPException(status_code=404, detail="Binding not found")
|
||||||
|
account = await session.get(ExternalChatAccount, binding.account_id)
|
||||||
|
if account is None or _is_inactive_whatsapp_account(account):
|
||||||
|
raise HTTPException(status_code=404, detail="Binding not found")
|
||||||
revoke_binding(binding)
|
revoke_binding(binding)
|
||||||
await session.commit()
|
await session.commit()
|
||||||
return {"ok": True}
|
return {"ok": True}
|
||||||
|
|
@ -929,6 +969,7 @@ async def delete_gateway_account(
|
||||||
or account.owner_user_id != user.id
|
or account.owner_user_id != user.id
|
||||||
or account.platform != ExternalChatPlatform.WHATSAPP
|
or account.platform != ExternalChatPlatform.WHATSAPP
|
||||||
or account.mode != ExternalChatAccountMode.SELF_HOST_BYO
|
or account.mode != ExternalChatAccountMode.SELF_HOST_BYO
|
||||||
|
or _is_inactive_whatsapp_account(account)
|
||||||
):
|
):
|
||||||
raise HTTPException(status_code=404, detail="Gateway account not found")
|
raise HTTPException(status_code=404, detail="Gateway account not found")
|
||||||
|
|
||||||
|
|
@ -961,6 +1002,9 @@ async def resume_external_chat_binding(
|
||||||
binding = await session.get(ExternalChatBinding, binding_id)
|
binding = await session.get(ExternalChatBinding, binding_id)
|
||||||
if binding is None or binding.user_id != user.id:
|
if binding is None or binding.user_id != user.id:
|
||||||
raise HTTPException(status_code=404, detail="Binding not found")
|
raise HTTPException(status_code=404, detail="Binding not found")
|
||||||
|
account = await session.get(ExternalChatAccount, binding.account_id)
|
||||||
|
if account is None or _is_inactive_whatsapp_account(account):
|
||||||
|
raise HTTPException(status_code=404, detail="Binding not found")
|
||||||
resume_binding(binding)
|
resume_binding(binding)
|
||||||
binding.updated_at = datetime.now(UTC)
|
binding.updated_at = datetime.now(UTC)
|
||||||
await session.commit()
|
await session.commit()
|
||||||
|
|
|
||||||
|
|
@ -208,12 +208,18 @@ export function MessagingChannelsContent() {
|
||||||
await refreshPlatform(connection.platform as GatewayPlatform);
|
await refreshPlatform(connection.platform as GatewayPlatform);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const isConnectionInActiveMode = (connection: GatewayConnection) => {
|
||||||
|
if (connection.platform !== "whatsapp") return true;
|
||||||
|
if (whatsappMode === "baileys") return connection.mode === "self_host_byo";
|
||||||
|
if (whatsappMode === "cloud") return connection.mode !== "self_host_byo";
|
||||||
|
return false;
|
||||||
|
};
|
||||||
const baileysQr = baileysHealth?.qr || null;
|
const baileysQr = baileysHealth?.qr || null;
|
||||||
const hasTelegramConnection = connections.some(
|
const hasTelegramConnection = connections.some(
|
||||||
(connection) => connection.platform === "telegram"
|
(connection) => connection.platform === "telegram"
|
||||||
);
|
);
|
||||||
const hasWhatsAppConnection = connections.some(
|
const hasWhatsAppConnection = connections.some(
|
||||||
(connection) => connection.platform === "whatsapp"
|
(connection) => connection.platform === "whatsapp" && isConnectionInActiveMode(connection)
|
||||||
);
|
);
|
||||||
const isRefreshing = (platform: GatewayPlatform) => refreshingPlatform === platform;
|
const isRefreshing = (platform: GatewayPlatform) => refreshingPlatform === platform;
|
||||||
const refreshButtonClassName = "gap-2";
|
const refreshButtonClassName = "gap-2";
|
||||||
|
|
@ -242,7 +248,7 @@ export function MessagingChannelsContent() {
|
||||||
`${platformLabel(connection.platform)} connection`;
|
`${platformLabel(connection.platform)} connection`;
|
||||||
const renderConnectionRows = (platform: GatewayConnection["platform"], emptyText: string) => {
|
const renderConnectionRows = (platform: GatewayConnection["platform"], emptyText: string) => {
|
||||||
const platformConnections = connections.filter(
|
const platformConnections = connections.filter(
|
||||||
(connection) => connection.platform === platform
|
(connection) => connection.platform === platform && isConnectionInActiveMode(connection)
|
||||||
);
|
);
|
||||||
|
|
||||||
if (platformConnections.length === 0) {
|
if (platformConnections.length === 0) {
|
||||||
|
|
@ -327,7 +333,7 @@ export function MessagingChannelsContent() {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="grid items-stretch gap-3 sm:grid-cols-2">
|
<div className="grid items-stretch gap-3 sm:grid-cols-2">
|
||||||
<Card className="group relative h-full overflow-hidden border-accent bg-accent/20 transition-all duration-200 hover:shadow-md">
|
<Card className="order-1 group relative h-full overflow-hidden border-accent bg-accent/20 transition-all duration-200 hover:shadow-md">
|
||||||
<CardHeader className="space-y-1.5 p-4 pb-2">
|
<CardHeader className="space-y-1.5 p-4 pb-2">
|
||||||
<div className="flex items-center justify-between gap-3">
|
<div className="flex items-center justify-between gap-3">
|
||||||
<CardTitle className="flex items-center gap-2 text-sm">Telegram</CardTitle>
|
<CardTitle className="flex items-center gap-2 text-sm">Telegram</CardTitle>
|
||||||
|
|
@ -360,7 +366,7 @@ export function MessagingChannelsContent() {
|
||||||
</Card>
|
</Card>
|
||||||
|
|
||||||
{slackGatewayEnabled ? (
|
{slackGatewayEnabled ? (
|
||||||
<Card className="group relative h-full overflow-hidden border-accent bg-accent/20 transition-all duration-200 hover:shadow-md">
|
<Card className="order-4 group relative h-full overflow-hidden border-accent bg-accent/20 transition-all duration-200 hover:shadow-md">
|
||||||
<CardHeader className="space-y-1.5 p-4 pb-2">
|
<CardHeader className="space-y-1.5 p-4 pb-2">
|
||||||
<div className="flex items-center justify-between gap-3">
|
<div className="flex items-center justify-between gap-3">
|
||||||
<CardTitle className="flex items-center gap-2 text-sm">Slack</CardTitle>
|
<CardTitle className="flex items-center gap-2 text-sm">Slack</CardTitle>
|
||||||
|
|
@ -392,7 +398,7 @@ export function MessagingChannelsContent() {
|
||||||
) : null}
|
) : null}
|
||||||
|
|
||||||
{discordGatewayEnabled ? (
|
{discordGatewayEnabled ? (
|
||||||
<Card className="group relative h-full overflow-hidden border-accent bg-accent/20 transition-all duration-200 hover:shadow-md">
|
<Card className="order-3 group relative h-full overflow-hidden border-accent bg-accent/20 transition-all duration-200 hover:shadow-md">
|
||||||
<CardHeader className="space-y-1.5 p-4 pb-2">
|
<CardHeader className="space-y-1.5 p-4 pb-2">
|
||||||
<div className="flex items-center justify-between gap-3">
|
<div className="flex items-center justify-between gap-3">
|
||||||
<CardTitle className="flex items-center gap-2 text-sm">Discord</CardTitle>
|
<CardTitle className="flex items-center gap-2 text-sm">Discord</CardTitle>
|
||||||
|
|
@ -424,16 +430,15 @@ export function MessagingChannelsContent() {
|
||||||
) : null}
|
) : null}
|
||||||
|
|
||||||
{whatsappMode !== "disabled" ? (
|
{whatsappMode !== "disabled" ? (
|
||||||
<Card className="group relative h-full overflow-hidden border-accent bg-accent/20 transition-all duration-200 hover:shadow-md">
|
<Card className="order-2 group relative h-full overflow-hidden border-accent bg-accent/20 transition-all duration-200 hover:shadow-md">
|
||||||
<CardHeader className="space-y-1.5 p-4 pb-2">
|
<CardHeader className="space-y-1.5 p-4 pb-2">
|
||||||
<div className="flex items-center justify-between gap-3">
|
<div className="flex items-center justify-between gap-3">
|
||||||
<CardTitle className="flex items-center gap-2 text-sm">WhatsApp</CardTitle>
|
<CardTitle className="flex items-center gap-2 text-sm">WhatsApp</CardTitle>
|
||||||
</div>
|
</div>
|
||||||
<p className="text-xs text-muted-foreground">
|
<p className="text-xs text-muted-foreground">
|
||||||
Pair this search space with WhatsApp using the configured gateway mode.
|
|
||||||
{whatsappMode === "baileys"
|
{whatsappMode === "baileys"
|
||||||
? " Send messages to your own WhatsApp chat. Other chats are ignored."
|
? "Use the WhatsApp bridge for your own WhatsApp chat. Other chats are ignored."
|
||||||
: ""}
|
: "Pair this search space with WhatsApp Cloud API."}
|
||||||
</p>
|
</p>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent className="space-y-3 p-4 pt-0">
|
<CardContent className="space-y-3 p-4 pt-0">
|
||||||
|
|
@ -469,7 +474,7 @@ export function MessagingChannelsContent() {
|
||||||
disabled={isRefreshing("whatsapp")}
|
disabled={isRefreshing("whatsapp")}
|
||||||
>
|
>
|
||||||
<RefreshCw className={refreshIconClassName("whatsapp")} />
|
<RefreshCw className={refreshIconClassName("whatsapp")} />
|
||||||
Refresh WhatsApp Bridge
|
Refresh
|
||||||
</Button>
|
</Button>
|
||||||
{baileysQr ? (
|
{baileysQr ? (
|
||||||
<div className="rounded-lg border border-accent bg-accent/20 p-3">
|
<div className="rounded-lg border border-accent bg-accent/20 p-3">
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue