diff --git a/docker/.env.example b/docker/.env.example index 71c743837..63308bc9e 100644 --- a/docker/.env.example +++ b/docker/.env.example @@ -281,6 +281,7 @@ STT_SERVICE=local/base # Messaging Channels (optional) # ------------------------------------------------------------------------------ # Configure only the external chat channels you want to use. +# GATEWAY_ENABLED=TRUE # -- Telegram -- # TELEGRAM_SHARED_BOT_TOKEN= diff --git a/surfsense_backend/.env.example b/surfsense_backend/.env.example index 1e09b266a..33aa09e83 100644 --- a/surfsense_backend/.env.example +++ b/surfsense_backend/.env.example @@ -15,12 +15,9 @@ CELERY_TASK_DEFAULT_QUEUE=surfsense # Optional: TTL in seconds for connector indexing lock key # CONNECTOR_INDEXING_LOCK_TTL_SECONDS=28800 -# Messaging Gateway (global) -# GATEWAY_ENABLED: master switch for ALL messaging gateway channels (Telegram, WhatsApp, -# Slack, Discord). When FALSE, no gateway background workers/supervisors start and all -# gateway HTTP routes (webhooks, OAuth callbacks, pairing) return 404. Set per-channel -# flags below to control individual platforms once the gateway is enabled. -GATEWAY_ENABLED=TRUE +# Messaging Gateway: disabled by default; set TRUE to enable chat integrations. +# Supported messaging gateways: WhatsApp, Telegram, Discord, Slack +# GATEWAY_ENABLED=TRUE # Telegram Gateway # TELEGRAM_WEBHOOK_SECRET must be 1-256 chars and contain only A-Z, a-z, 0-9, _ or - @@ -386,7 +383,9 @@ LANGSMITH_PROJECT=surfsense # SURFSENSE_ENABLE_LLM_TOOL_SELECTOR=false # adds a per-turn LLM call # Observability - OTel -# SURFSENSE_ENABLE_OTEL=false +# Disabled by default. Uncomment to enable OpenTelemetry. +# SURFSENSE_ENABLE_OTEL=true + # OpenTelemetry - endpoint enables export; absent = no-op. # Production should point at an OTel Collector. For local docker-compose.dev.yml, # use http://otel-lgtm:4317 instead. diff --git a/surfsense_backend/app/config/__init__.py b/surfsense_backend/app/config/__init__.py index 5385d12fc..428a37377 100644 --- a/surfsense_backend/app/config/__init__.py +++ b/surfsense_backend/app/config/__init__.py @@ -540,10 +540,10 @@ class Config: # Backend URL to override the http to https in the OAuth redirect URI BACKEND_URL = os.getenv("BACKEND_URL") or SURFSENSE_PUBLIC_URL - # Messaging gateway (Telegram v1) + # Messaging gateway # Global master switch: when FALSE, no gateway supervisors/workers start and all - # gateway HTTP routes return 404, regardless of the per-channel flags below. - GATEWAY_ENABLED = os.getenv("GATEWAY_ENABLED", "TRUE").upper() == "TRUE" + # gated gateway HTTP routes return 404, regardless of the per-channel flags below. + GATEWAY_ENABLED = os.getenv("GATEWAY_ENABLED", "FALSE").upper() == "TRUE" TELEGRAM_SHARED_BOT_TOKEN = os.getenv("TELEGRAM_SHARED_BOT_TOKEN") TELEGRAM_SHARED_BOT_USERNAME = os.getenv("TELEGRAM_SHARED_BOT_USERNAME") TELEGRAM_WEBHOOK_SECRET = os.getenv("TELEGRAM_WEBHOOK_SECRET") diff --git a/surfsense_backend/app/gateway/__init__.py b/surfsense_backend/app/gateway/__init__.py index 8b79b3160..89b931bc3 100644 --- a/surfsense_backend/app/gateway/__init__.py +++ b/surfsense_backend/app/gateway/__init__.py @@ -8,7 +8,7 @@ from app.config import config def require_gateway_enabled() -> None: - """FastAPI dependency that gates all gateway HTTP routes on the global flag. + """FastAPI dependency that gates gateway operational routes on the global flag. Returns 404 (rather than 503) when ``GATEWAY_ENABLED`` is FALSE so that disabling the gateway makes its webhook/OAuth/pairing surface indistinguishable diff --git a/surfsense_backend/app/routes/__init__.py b/surfsense_backend/app/routes/__init__.py index 0da3bd251..8ce84d179 100644 --- a/surfsense_backend/app/routes/__init__.py +++ b/surfsense_backend/app/routes/__init__.py @@ -24,7 +24,10 @@ from .dropbox_add_connector_route import router as dropbox_add_connector_router from .editor_routes import router as editor_router from .export_routes import router as export_router from .folders_routes import router as folders_router -from .gateway_webhook_routes import router as gateway_router +from .gateway_webhook_routes import ( + config_router as gateway_config_router, + router as gateway_router, +) from .gateway_whatsapp_baileys_routes import router as gateway_whatsapp_baileys_router from .gateway_whatsapp_webhook_routes import router as gateway_whatsapp_webhook_router from .google_calendar_add_connector_route import ( @@ -74,6 +77,7 @@ router.include_router(export_router) router.include_router(documents_router) router.include_router(folders_router) _gateway_enabled_dep = [Depends(require_gateway_enabled)] +router.include_router(gateway_config_router) router.include_router(gateway_router, dependencies=_gateway_enabled_dep) router.include_router( gateway_whatsapp_webhook_router, dependencies=_gateway_enabled_dep diff --git a/surfsense_backend/app/routes/gateway_webhook_routes.py b/surfsense_backend/app/routes/gateway_webhook_routes.py index 14f929567..9b4af4b83 100644 --- a/surfsense_backend/app/routes/gateway_webhook_routes.py +++ b/surfsense_backend/app/routes/gateway_webhook_routes.py @@ -56,6 +56,7 @@ from app.utils.oauth_security import OAuthStateManager, TokenEncryption from app.utils.rbac import check_search_space_access router = APIRouter(prefix="/gateway", tags=["gateway"]) +config_router = APIRouter(prefix="/gateway", tags=["gateway"]) logger = logging.getLogger(__name__) SLACK_AUTHORIZATION_URL = "https://slack.com/oauth/v2/authorize" @@ -967,11 +968,20 @@ async def list_platforms( ] -@router.get("/config") +@config_router.get("/config") async def get_gateway_config( user: User = Depends(current_active_user), ) -> dict[str, bool | str]: + if not config.GATEWAY_ENABLED: + return { + "enabled": False, + "telegram_enabled": False, + "whatsapp_intake_mode": "disabled", + "slack_enabled": False, + "discord_enabled": False, + } return { + "enabled": True, "telegram_enabled": _telegram_gateway_enabled(), "whatsapp_intake_mode": config.GATEWAY_WHATSAPP_INTAKE_MODE, "slack_enabled": _slack_gateway_enabled(), diff --git a/surfsense_web/app/dashboard/[search_space_id]/user-settings/components/MessagingChannelsContent.tsx b/surfsense_web/app/dashboard/[search_space_id]/user-settings/components/MessagingChannelsContent.tsx index 138a2aad1..faa2d00e9 100644 --- a/surfsense_web/app/dashboard/[search_space_id]/user-settings/components/MessagingChannelsContent.tsx +++ b/surfsense_web/app/dashboard/[search_space_id]/user-settings/components/MessagingChannelsContent.tsx @@ -1,6 +1,6 @@ "use client"; -import { RefreshCw, ShieldAlert } from "lucide-react"; +import { MessageCircle, RefreshCw, ShieldAlert } from "lucide-react"; import { useParams } from "next/navigation"; import { QRCodeSVG } from "qrcode.react"; import { useCallback, useEffect, useState } from "react"; @@ -39,6 +39,7 @@ type GatewayConnection = { }; type GatewayConfig = { + enabled: boolean; telegram_enabled: boolean; whatsapp_intake_mode: "disabled" | "cloud" | "baileys"; slack_enabled: boolean; @@ -47,6 +48,14 @@ type GatewayConfig = { type GatewayConfigState = GatewayConfig | null; +const DISABLED_GATEWAY_CONFIG: GatewayConfig = { + enabled: false, + telegram_enabled: false, + whatsapp_intake_mode: "disabled", + slack_enabled: false, + discord_enabled: false, +}; + type Pairing = { binding_id: number; code: string; @@ -80,17 +89,26 @@ export function MessagingChannelsContent() { const whatsappMode = gatewayConfig?.whatsapp_intake_mode ?? "disabled"; const slackGatewayEnabled = gatewayConfig?.slack_enabled ?? false; const discordGatewayEnabled = gatewayConfig?.discord_enabled ?? false; + const gatewayDisabled = gatewayConfig?.enabled === false; const fetchConnections = useCallback(async (platform?: GatewayPlatform) => { const res = await authenticatedFetch( buildBackendUrl("/api/v1/gateway/connections", platform ? { platform } : undefined) ); - return (await res.json()) as GatewayConnection[]; + if (!res.ok) return []; + const data = await res.json(); + return Array.isArray(data) ? (data as GatewayConnection[]) : []; }, []); - const fetchGatewayConfig = useCallback(async () => { + const fetchGatewayConfig = useCallback(async (): Promise => { const res = await authenticatedFetch(buildBackendUrl("/api/v1/gateway/config")); - return (await res.json()) as GatewayConfig; + if (!res.ok) return DISABLED_GATEWAY_CONFIG; + const data = (await res.json()) as Partial; + return { + ...DISABLED_GATEWAY_CONFIG, + ...data, + enabled: data.enabled ?? true, + }; }, []); const refresh = useCallback(async () => { @@ -225,12 +243,9 @@ export function MessagingChannelsContent() { } async function resume(connection: GatewayConnection) { - await authenticatedFetch( - buildBackendUrl(`/api/v1/gateway/bindings/${connection.id}/resume`), - { - method: "POST", - } - ); + await authenticatedFetch(buildBackendUrl(`/api/v1/gateway/bindings/${connection.id}/resume`), { + method: "POST", + }); await refreshPlatform(connection.platform as GatewayPlatform); } @@ -387,7 +402,27 @@ export function MessagingChannelsContent() {
{isGatewayConfigLoading ? renderGatewaySkeletons() : null} - {!isGatewayConfigLoading && !hasEnabledGateway ? ( + {!isGatewayConfigLoading && gatewayDisabled ? ( + + +
+ + Messaging Channels coming soon +
+

+ Soon you'll be able to connect WhatsApp, Telegram, Slack, and Discord to your + SurfSense agent so you can ask questions, route messages to search spaces, and get + answers from your knowledge base without leaving your chat app. +

+
+ +

Pair a chat once, then DM the SurfSense agent like a teammate.

+

Each channel can be routed to the right search space when integrations launch.

+
+
+ ) : null} + + {!isGatewayConfigLoading && !gatewayDisabled && !hasEnabledGateway ? ( No messaging gateways enabled @@ -395,7 +430,7 @@ export function MessagingChannelsContent() { ) : null} - {telegramGatewayEnabled ? ( + {!gatewayDisabled && telegramGatewayEnabled ? (
@@ -431,7 +466,7 @@ export function MessagingChannelsContent() { ) : null} - {slackGatewayEnabled ? ( + {!gatewayDisabled && slackGatewayEnabled ? (
@@ -463,7 +498,7 @@ export function MessagingChannelsContent() { ) : null} - {discordGatewayEnabled ? ( + {!gatewayDisabled && discordGatewayEnabled ? (
@@ -495,7 +530,7 @@ export function MessagingChannelsContent() { ) : null} - {whatsappMode !== "disabled" ? ( + {!gatewayDisabled && whatsappMode !== "disabled" ? (