mirror of
https://github.com/MODSetter/SurfSense.git
synced 2026-06-02 19:55:18 +02:00
feat(gateway): add Slack and Telegram gateway configuration and enablement checks
This commit is contained in:
parent
fc2467be3d
commit
799a83239f
6 changed files with 168 additions and 62 deletions
|
|
@ -285,7 +285,6 @@ services:
|
||||||
NEXT_PUBLIC_FASTAPI_BACKEND_AUTH_TYPE: ${AUTH_TYPE:-LOCAL}
|
NEXT_PUBLIC_FASTAPI_BACKEND_AUTH_TYPE: ${AUTH_TYPE:-LOCAL}
|
||||||
NEXT_PUBLIC_ETL_SERVICE: ${ETL_SERVICE:-DOCLING}
|
NEXT_PUBLIC_ETL_SERVICE: ${ETL_SERVICE:-DOCLING}
|
||||||
NEXT_PUBLIC_DEPLOYMENT_MODE: ${DEPLOYMENT_MODE:-self-hosted}
|
NEXT_PUBLIC_DEPLOYMENT_MODE: ${DEPLOYMENT_MODE:-self-hosted}
|
||||||
NEXT_PUBLIC_GATEWAY_WHATSAPP_INTAKE_MODE: ${GATEWAY_WHATSAPP_INTAKE_MODE:-disabled}
|
|
||||||
NEXT_PUBLIC_WHATSAPP_DISPLAY_PHONE_NUMBER: ${WHATSAPP_SHARED_DISPLAY_PHONE_NUMBER:-}
|
NEXT_PUBLIC_WHATSAPP_DISPLAY_PHONE_NUMBER: ${WHATSAPP_SHARED_DISPLAY_PHONE_NUMBER:-}
|
||||||
FASTAPI_BACKEND_INTERNAL_URL: ${FASTAPI_BACKEND_INTERNAL_URL:-http://backend:8000}
|
FASTAPI_BACKEND_INTERNAL_URL: ${FASTAPI_BACKEND_INTERNAL_URL:-http://backend:8000}
|
||||||
labels:
|
labels:
|
||||||
|
|
|
||||||
|
|
@ -17,7 +17,7 @@ REDIS_APP_URL=redis://localhost:6379/0
|
||||||
|
|
||||||
# Telegram Gateway
|
# Telegram Gateway
|
||||||
# TELEGRAM_WEBHOOK_SECRET must be 1-256 chars and contain only A-Z, a-z, 0-9, _ or -
|
# TELEGRAM_WEBHOOK_SECRET must be 1-256 chars and contain only A-Z, a-z, 0-9, _ or -
|
||||||
# GATEWAY_TELEGRAM_INTAKE_MODE: webhook for production, longpoll for single-replica self-host fallback, disabled to skip Telegram intake
|
# GATEWAY_TELEGRAM_INTAKE_MODE: `webhook` for production, `longpoll` for single-replica self-host fallback, `disabled` to skip Telegram intake
|
||||||
TELEGRAM_SHARED_BOT_TOKEN=
|
TELEGRAM_SHARED_BOT_TOKEN=
|
||||||
TELEGRAM_SHARED_BOT_USERNAME=
|
TELEGRAM_SHARED_BOT_USERNAME=
|
||||||
TELEGRAM_WEBHOOK_SECRET=
|
TELEGRAM_WEBHOOK_SECRET=
|
||||||
|
|
@ -25,7 +25,7 @@ GATEWAY_BASE_URL=http://localhost:8000
|
||||||
GATEWAY_TELEGRAM_INTAKE_MODE=webhook
|
GATEWAY_TELEGRAM_INTAKE_MODE=webhook
|
||||||
|
|
||||||
# WhatsApp Gateway
|
# WhatsApp Gateway
|
||||||
# GATEWAY_WHATSAPP_INTAKE_MODE: cloud for Meta Cloud API, baileys for self-hosted bridge, disabled to skip WhatsApp intake
|
# GATEWAY_WHATSAPP_INTAKE_MODE: `cloud` for Meta Cloud API, `baileys` for self-hosted bridge, `disabled` to skip WhatsApp intake
|
||||||
GATEWAY_WHATSAPP_INTAKE_MODE=disabled
|
GATEWAY_WHATSAPP_INTAKE_MODE=disabled
|
||||||
WHATSAPP_SHARED_BUSINESS_TOKEN=
|
WHATSAPP_SHARED_BUSINESS_TOKEN=
|
||||||
WHATSAPP_SHARED_PHONE_NUMBER_ID=
|
WHATSAPP_SHARED_PHONE_NUMBER_ID=
|
||||||
|
|
@ -149,6 +149,7 @@ NOTION_REDIRECT_URI=http://localhost:8000/api/v1/auth/notion/connector/callback
|
||||||
SLACK_CLIENT_ID=your_slack_client_id_here
|
SLACK_CLIENT_ID=your_slack_client_id_here
|
||||||
SLACK_CLIENT_SECRET=your_slack_client_secret_here
|
SLACK_CLIENT_SECRET=your_slack_client_secret_here
|
||||||
SLACK_REDIRECT_URI=http://localhost:8000/api/v1/auth/slack/connector/callback
|
SLACK_REDIRECT_URI=http://localhost:8000/api/v1/auth/slack/connector/callback
|
||||||
|
GATEWAY_SLACK_ENABLED=FALSE
|
||||||
GATEWAY_SLACK_SIGNING_SECRET=your_slack_signing_secret_here
|
GATEWAY_SLACK_SIGNING_SECRET=your_slack_signing_secret_here
|
||||||
GATEWAY_SLACK_REDIRECT_URI=http://localhost:8000/api/v1/gateway/slack/callback
|
GATEWAY_SLACK_REDIRECT_URI=http://localhost:8000/api/v1/gateway/slack/callback
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -572,6 +572,7 @@ class Config:
|
||||||
)
|
)
|
||||||
GATEWAY_SLACK_CLIENT_ID = os.getenv("SLACK_CLIENT_ID")
|
GATEWAY_SLACK_CLIENT_ID = os.getenv("SLACK_CLIENT_ID")
|
||||||
GATEWAY_SLACK_CLIENT_SECRET = os.getenv("SLACK_CLIENT_SECRET")
|
GATEWAY_SLACK_CLIENT_SECRET = os.getenv("SLACK_CLIENT_SECRET")
|
||||||
|
GATEWAY_SLACK_ENABLED = os.getenv("GATEWAY_SLACK_ENABLED", "FALSE").upper() == "TRUE"
|
||||||
GATEWAY_SLACK_SIGNING_SECRET = os.getenv("GATEWAY_SLACK_SIGNING_SECRET")
|
GATEWAY_SLACK_SIGNING_SECRET = os.getenv("GATEWAY_SLACK_SIGNING_SECRET")
|
||||||
GATEWAY_SLACK_REDIRECT_URI = os.getenv("GATEWAY_SLACK_REDIRECT_URI")
|
GATEWAY_SLACK_REDIRECT_URI = os.getenv("GATEWAY_SLACK_REDIRECT_URI")
|
||||||
GATEWAY_DISCORD_ENABLED = (
|
GATEWAY_DISCORD_ENABLED = (
|
||||||
|
|
|
||||||
|
|
@ -188,6 +188,36 @@ def _is_inactive_whatsapp_account(account: ExternalChatAccount) -> bool:
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _telegram_gateway_enabled() -> bool:
|
||||||
|
return (
|
||||||
|
config.GATEWAY_TELEGRAM_INTAKE_MODE != "disabled"
|
||||||
|
and bool(config.TELEGRAM_SHARED_BOT_TOKEN)
|
||||||
|
and bool(config.TELEGRAM_SHARED_BOT_USERNAME)
|
||||||
|
and (
|
||||||
|
config.GATEWAY_TELEGRAM_INTAKE_MODE != "webhook"
|
||||||
|
or bool(config.TELEGRAM_WEBHOOK_SECRET)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _slack_gateway_enabled() -> bool:
|
||||||
|
return bool(
|
||||||
|
config.GATEWAY_SLACK_ENABLED
|
||||||
|
and config.GATEWAY_SLACK_CLIENT_ID
|
||||||
|
and config.GATEWAY_SLACK_CLIENT_SECRET
|
||||||
|
and config.GATEWAY_SLACK_SIGNING_SECRET
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _discord_gateway_enabled() -> bool:
|
||||||
|
return bool(
|
||||||
|
config.GATEWAY_DISCORD_ENABLED
|
||||||
|
and config.DISCORD_CLIENT_ID
|
||||||
|
and config.DISCORD_CLIENT_SECRET
|
||||||
|
and config.DISCORD_BOT_TOKEN
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
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"
|
||||||
|
|
@ -208,7 +238,7 @@ async def install_slack_gateway(
|
||||||
user: User = Depends(current_active_user),
|
user: User = Depends(current_active_user),
|
||||||
session: AsyncSession = Depends(get_async_session),
|
session: AsyncSession = Depends(get_async_session),
|
||||||
) -> dict[str, str]:
|
) -> dict[str, str]:
|
||||||
if not config.GATEWAY_SLACK_CLIENT_ID:
|
if not _slack_gateway_enabled():
|
||||||
raise HTTPException(status_code=500, detail="Slack gateway OAuth is not configured")
|
raise HTTPException(status_code=500, detail="Slack gateway OAuth is not configured")
|
||||||
await check_search_space_access(session, user, search_space_id)
|
await check_search_space_access(session, user, search_space_id)
|
||||||
state = _get_state_manager().generate_secure_state(search_space_id, user.id)
|
state = _get_state_manager().generate_secure_state(search_space_id, user.id)
|
||||||
|
|
@ -242,7 +272,7 @@ async def slack_gateway_callback(
|
||||||
return _slack_frontend_redirect(space_id or 0, error="slack_gateway_oauth_denied")
|
return _slack_frontend_redirect(space_id or 0, error="slack_gateway_oauth_denied")
|
||||||
if not code or state_data is None:
|
if not code or state_data is None:
|
||||||
raise HTTPException(status_code=400, detail="Invalid Slack gateway OAuth callback")
|
raise HTTPException(status_code=400, detail="Invalid Slack gateway OAuth callback")
|
||||||
if not config.GATEWAY_SLACK_CLIENT_ID or not config.GATEWAY_SLACK_CLIENT_SECRET:
|
if not _slack_gateway_enabled():
|
||||||
raise HTTPException(status_code=500, detail="Slack gateway OAuth is not configured")
|
raise HTTPException(status_code=500, detail="Slack gateway OAuth is not configured")
|
||||||
|
|
||||||
user_id = UUID(state_data["user_id"])
|
user_id = UUID(state_data["user_id"])
|
||||||
|
|
@ -357,7 +387,7 @@ async def install_discord_gateway(
|
||||||
user: User = Depends(current_active_user),
|
user: User = Depends(current_active_user),
|
||||||
session: AsyncSession = Depends(get_async_session),
|
session: AsyncSession = Depends(get_async_session),
|
||||||
) -> dict[str, str]:
|
) -> dict[str, str]:
|
||||||
if not config.DISCORD_CLIENT_ID:
|
if not _discord_gateway_enabled():
|
||||||
raise HTTPException(status_code=500, detail="Discord gateway OAuth is not configured")
|
raise HTTPException(status_code=500, detail="Discord gateway OAuth is not configured")
|
||||||
await check_search_space_access(session, user, search_space_id)
|
await check_search_space_access(session, user, search_space_id)
|
||||||
state = _get_state_manager().generate_secure_state(search_space_id, user.id)
|
state = _get_state_manager().generate_secure_state(search_space_id, user.id)
|
||||||
|
|
@ -393,10 +423,8 @@ async def discord_gateway_callback(
|
||||||
return _discord_frontend_redirect(space_id or 0, error="discord_gateway_oauth_denied")
|
return _discord_frontend_redirect(space_id or 0, error="discord_gateway_oauth_denied")
|
||||||
if not code or state_data is None:
|
if not code or state_data is None:
|
||||||
raise HTTPException(status_code=400, detail="Invalid Discord gateway OAuth callback")
|
raise HTTPException(status_code=400, detail="Invalid Discord gateway OAuth callback")
|
||||||
if not config.DISCORD_CLIENT_ID or not config.DISCORD_CLIENT_SECRET:
|
if not _discord_gateway_enabled():
|
||||||
raise HTTPException(status_code=500, detail="Discord gateway OAuth is not configured")
|
raise HTTPException(status_code=500, detail="Discord gateway OAuth is not configured")
|
||||||
if not config.DISCORD_BOT_TOKEN:
|
|
||||||
raise HTTPException(status_code=500, detail="Discord gateway bot token is not configured")
|
|
||||||
|
|
||||||
user_id = UUID(state_data["user_id"])
|
user_id = UUID(state_data["user_id"])
|
||||||
token_payload = {
|
token_payload = {
|
||||||
|
|
@ -518,6 +546,9 @@ async def slack_webhook(
|
||||||
request: Request,
|
request: Request,
|
||||||
session: AsyncSession = Depends(get_async_session),
|
session: AsyncSession = Depends(get_async_session),
|
||||||
) -> Response:
|
) -> Response:
|
||||||
|
if not _slack_gateway_enabled():
|
||||||
|
return Response(status_code=200)
|
||||||
|
|
||||||
body = await request.body()
|
body = await request.body()
|
||||||
if not verify_slack_signature(
|
if not verify_slack_signature(
|
||||||
signing_secret=config.GATEWAY_SLACK_SIGNING_SECRET or "",
|
signing_secret=config.GATEWAY_SLACK_SIGNING_SECRET or "",
|
||||||
|
|
@ -594,6 +625,9 @@ async def telegram_webhook(
|
||||||
account_id: int,
|
account_id: int,
|
||||||
session: AsyncSession = Depends(get_async_session),
|
session: AsyncSession = Depends(get_async_session),
|
||||||
) -> Response:
|
) -> Response:
|
||||||
|
if not _telegram_gateway_enabled():
|
||||||
|
return Response(status_code=200)
|
||||||
|
|
||||||
request_id = f"gateway_{uuid.uuid4().hex[:16]}"
|
request_id = f"gateway_{uuid.uuid4().hex[:16]}"
|
||||||
try:
|
try:
|
||||||
payload = await request.json()
|
payload = await request.json()
|
||||||
|
|
@ -644,6 +678,8 @@ async def start_binding(
|
||||||
await check_search_space_access(session, user, body.search_space_id)
|
await check_search_space_access(session, user, body.search_space_id)
|
||||||
code = generate_pairing_code()
|
code = generate_pairing_code()
|
||||||
if body.platform == ExternalChatPlatform.TELEGRAM:
|
if body.platform == ExternalChatPlatform.TELEGRAM:
|
||||||
|
if not _telegram_gateway_enabled():
|
||||||
|
raise HTTPException(status_code=400, detail="Telegram gateway is disabled")
|
||||||
account = await get_or_create_system_telegram_account(session)
|
account = await get_or_create_system_telegram_account(session)
|
||||||
username = account.bot_username or config.TELEGRAM_SHARED_BOT_USERNAME
|
username = account.bot_username or config.TELEGRAM_SHARED_BOT_USERNAME
|
||||||
if not username:
|
if not username:
|
||||||
|
|
@ -730,6 +766,8 @@ async def list_connections(
|
||||||
active_whatsapp_mode = _active_whatsapp_account_mode()
|
active_whatsapp_mode = _active_whatsapp_account_mode()
|
||||||
if platform == ExternalChatPlatform.WHATSAPP and active_whatsapp_mode is None:
|
if platform == ExternalChatPlatform.WHATSAPP and active_whatsapp_mode is None:
|
||||||
return []
|
return []
|
||||||
|
if platform == ExternalChatPlatform.TELEGRAM and not _telegram_gateway_enabled():
|
||||||
|
return []
|
||||||
|
|
||||||
filters = [
|
filters = [
|
||||||
ExternalChatBinding.user_id == user.id,
|
ExternalChatBinding.user_id == user.id,
|
||||||
|
|
@ -741,15 +779,18 @@ async def list_connections(
|
||||||
filters.append(ExternalChatAccount.platform == platform)
|
filters.append(ExternalChatAccount.platform == platform)
|
||||||
if platform == ExternalChatPlatform.WHATSAPP and active_whatsapp_mode is not None:
|
if platform == ExternalChatPlatform.WHATSAPP and active_whatsapp_mode is not None:
|
||||||
filters.append(ExternalChatAccount.mode == active_whatsapp_mode)
|
filters.append(ExternalChatAccount.mode == active_whatsapp_mode)
|
||||||
elif active_whatsapp_mode is None:
|
|
||||||
filters.append(ExternalChatAccount.platform != ExternalChatPlatform.WHATSAPP)
|
|
||||||
else:
|
else:
|
||||||
filters.append(
|
if not _telegram_gateway_enabled():
|
||||||
or_(
|
filters.append(ExternalChatAccount.platform != ExternalChatPlatform.TELEGRAM)
|
||||||
ExternalChatAccount.platform != ExternalChatPlatform.WHATSAPP,
|
if active_whatsapp_mode is None:
|
||||||
ExternalChatAccount.mode == active_whatsapp_mode,
|
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)
|
||||||
|
|
@ -873,6 +914,18 @@ async def list_platforms(
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("/config")
|
||||||
|
async def get_gateway_config(
|
||||||
|
user: User = Depends(current_active_user),
|
||||||
|
) -> dict[str, bool | str]:
|
||||||
|
return {
|
||||||
|
"telegram_enabled": _telegram_gateway_enabled(),
|
||||||
|
"whatsapp_intake_mode": config.GATEWAY_WHATSAPP_INTAKE_MODE,
|
||||||
|
"slack_enabled": _slack_gateway_enabled(),
|
||||||
|
"discord_enabled": _discord_gateway_enabled(),
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
@router.patch("/bindings/{binding_id}/search-space")
|
@router.patch("/bindings/{binding_id}/search-space")
|
||||||
async def update_binding_search_space(
|
async def update_binding_search_space(
|
||||||
binding_id: int,
|
binding_id: int,
|
||||||
|
|
|
||||||
|
|
@ -7,14 +7,6 @@ NEXT_PUBLIC_FASTAPI_BACKEND_AUTH_TYPE=LOCAL or GOOGLE
|
||||||
NEXT_PUBLIC_ETL_SERVICE=UNSTRUCTURED or LLAMACLOUD or DOCLING
|
NEXT_PUBLIC_ETL_SERVICE=UNSTRUCTURED or LLAMACLOUD or DOCLING
|
||||||
NEXT_PUBLIC_ZERO_CACHE_URL=http://localhost:4848
|
NEXT_PUBLIC_ZERO_CACHE_URL=http://localhost:4848
|
||||||
|
|
||||||
# Messaging gateway options
|
|
||||||
# WhatsApp UI toggle: disabled, cloud, or baileys
|
|
||||||
NEXT_PUBLIC_GATEWAY_WHATSAPP_INTAKE_MODE=disabled
|
|
||||||
# Slack gateway UI toggle: true or false
|
|
||||||
NEXT_PUBLIC_GATEWAY_SLACK_ENABLED=false
|
|
||||||
# Discord gateway UI toggle: true or false
|
|
||||||
NEXT_PUBLIC_GATEWAY_DISCORD_ENABLED=false
|
|
||||||
|
|
||||||
# Contact Form Vars (optional)
|
# Contact Form Vars (optional)
|
||||||
DATABASE_URL=postgresql://postgres:[YOUR-PASSWORD]@db.sdsf.supabase.co:5432/postgres
|
DATABASE_URL=postgresql://postgres:[YOUR-PASSWORD]@db.sdsf.supabase.co:5432/postgres
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -15,6 +15,7 @@ import {
|
||||||
SelectValue,
|
SelectValue,
|
||||||
} from "@/components/ui/select";
|
} from "@/components/ui/select";
|
||||||
import { Separator } from "@/components/ui/separator";
|
import { Separator } from "@/components/ui/separator";
|
||||||
|
import { Skeleton } from "@/components/ui/skeleton";
|
||||||
import type { SearchSpace } from "@/contracts/types/search-space.types";
|
import type { SearchSpace } from "@/contracts/types/search-space.types";
|
||||||
import { searchSpacesApiService } from "@/lib/apis/search-spaces-api.service";
|
import { searchSpacesApiService } from "@/lib/apis/search-spaces-api.service";
|
||||||
import { authenticatedFetch } from "@/lib/auth-utils";
|
import { authenticatedFetch } from "@/lib/auth-utils";
|
||||||
|
|
@ -37,6 +38,15 @@ type GatewayConnection = {
|
||||||
suspended_reason?: string | null;
|
suspended_reason?: string | null;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
type GatewayConfig = {
|
||||||
|
telegram_enabled: boolean;
|
||||||
|
whatsapp_intake_mode: "disabled" | "cloud" | "baileys";
|
||||||
|
slack_enabled: boolean;
|
||||||
|
discord_enabled: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
type GatewayConfigState = GatewayConfig | null;
|
||||||
|
|
||||||
type Pairing = {
|
type Pairing = {
|
||||||
binding_id: number;
|
binding_id: number;
|
||||||
code: string;
|
code: string;
|
||||||
|
|
@ -58,15 +68,18 @@ type BaileysHealth = {
|
||||||
export function MessagingChannelsContent() {
|
export function MessagingChannelsContent() {
|
||||||
const params = useParams<{ search_space_id: string }>();
|
const params = useParams<{ search_space_id: string }>();
|
||||||
const searchSpaceId = Number(params.search_space_id);
|
const searchSpaceId = Number(params.search_space_id);
|
||||||
const whatsappMode = process.env.NEXT_PUBLIC_GATEWAY_WHATSAPP_INTAKE_MODE ?? "disabled";
|
const [gatewayConfig, setGatewayConfig] = useState<GatewayConfigState>(null);
|
||||||
const slackGatewayEnabled = process.env.NEXT_PUBLIC_GATEWAY_SLACK_ENABLED === "true";
|
|
||||||
const discordGatewayEnabled = process.env.NEXT_PUBLIC_GATEWAY_DISCORD_ENABLED === "true";
|
|
||||||
const [connections, setConnections] = useState<GatewayConnection[]>([]);
|
const [connections, setConnections] = useState<GatewayConnection[]>([]);
|
||||||
const [searchSpaces, setSearchSpaces] = useState<SearchSpace[]>([]);
|
const [searchSpaces, setSearchSpaces] = useState<SearchSpace[]>([]);
|
||||||
const [pairing, setPairing] = useState<Pairing | null>(null);
|
const [pairing, setPairing] = useState<Pairing | null>(null);
|
||||||
const [pairingPlatform, setPairingPlatform] = useState<PairingPlatform | null>(null);
|
const [pairingPlatform, setPairingPlatform] = useState<PairingPlatform | null>(null);
|
||||||
const [baileysHealth, setBaileysHealth] = useState<BaileysHealth | null>(null);
|
const [baileysHealth, setBaileysHealth] = useState<BaileysHealth | null>(null);
|
||||||
const [refreshingPlatform, setRefreshingPlatform] = useState<GatewayPlatform | null>(null);
|
const [refreshingPlatform, setRefreshingPlatform] = useState<GatewayPlatform | null>(null);
|
||||||
|
const isGatewayConfigLoading = gatewayConfig === null;
|
||||||
|
const telegramGatewayEnabled = gatewayConfig?.telegram_enabled ?? false;
|
||||||
|
const whatsappMode = gatewayConfig?.whatsapp_intake_mode ?? "disabled";
|
||||||
|
const slackGatewayEnabled = gatewayConfig?.slack_enabled ?? false;
|
||||||
|
const discordGatewayEnabled = gatewayConfig?.discord_enabled ?? false;
|
||||||
|
|
||||||
const fetchConnections = useCallback(async (platform?: GatewayPlatform) => {
|
const fetchConnections = useCallback(async (platform?: GatewayPlatform) => {
|
||||||
const query = platform ? `?platform=${encodeURIComponent(platform)}` : "";
|
const query = platform ? `?platform=${encodeURIComponent(platform)}` : "";
|
||||||
|
|
@ -74,14 +87,21 @@ export function MessagingChannelsContent() {
|
||||||
return (await res.json()) as GatewayConnection[];
|
return (await res.json()) as GatewayConnection[];
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
const fetchGatewayConfig = useCallback(async () => {
|
||||||
|
const res = await authenticatedFetch(`${BACKEND_URL}/api/v1/gateway/config`);
|
||||||
|
return (await res.json()) as GatewayConfig;
|
||||||
|
}, []);
|
||||||
|
|
||||||
const refresh = useCallback(async () => {
|
const refresh = useCallback(async () => {
|
||||||
const [nextConnections, spaces] = await Promise.all([
|
const [nextConnections, spaces, nextGatewayConfig] = await Promise.all([
|
||||||
fetchConnections(),
|
fetchConnections(),
|
||||||
searchSpacesApiService.getSearchSpaces(),
|
searchSpacesApiService.getSearchSpaces(),
|
||||||
|
fetchGatewayConfig(),
|
||||||
]);
|
]);
|
||||||
setConnections(nextConnections);
|
setConnections(nextConnections);
|
||||||
setSearchSpaces(spaces);
|
setSearchSpaces(spaces);
|
||||||
}, [fetchConnections]);
|
setGatewayConfig(nextGatewayConfig);
|
||||||
|
}, [fetchConnections, fetchGatewayConfig]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
void refresh();
|
void refresh();
|
||||||
|
|
@ -221,6 +241,11 @@ export function MessagingChannelsContent() {
|
||||||
const hasWhatsAppConnection = connections.some(
|
const hasWhatsAppConnection = connections.some(
|
||||||
(connection) => connection.platform === "whatsapp" && isConnectionInActiveMode(connection)
|
(connection) => connection.platform === "whatsapp" && isConnectionInActiveMode(connection)
|
||||||
);
|
);
|
||||||
|
const hasEnabledGateway =
|
||||||
|
telegramGatewayEnabled ||
|
||||||
|
whatsappMode !== "disabled" ||
|
||||||
|
slackGatewayEnabled ||
|
||||||
|
discordGatewayEnabled;
|
||||||
const isRefreshing = (platform: GatewayPlatform) => refreshingPlatform === platform;
|
const isRefreshing = (platform: GatewayPlatform) => refreshingPlatform === platform;
|
||||||
const refreshButtonClassName = "gap-2";
|
const refreshButtonClassName = "gap-2";
|
||||||
const refreshIconClassName = (platform: GatewayPlatform) =>
|
const refreshIconClassName = (platform: GatewayPlatform) =>
|
||||||
|
|
@ -252,7 +277,11 @@ export function MessagingChannelsContent() {
|
||||||
);
|
);
|
||||||
|
|
||||||
if (platformConnections.length === 0) {
|
if (platformConnections.length === 0) {
|
||||||
return <p className="text-xs text-muted-foreground">{emptyText}</p>;
|
return (
|
||||||
|
<div className="flex min-h-24 items-center justify-center text-center">
|
||||||
|
<p className="text-xs text-muted-foreground">{emptyText}</p>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|
@ -330,40 +359,71 @@ export function MessagingChannelsContent() {
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
const renderGatewaySkeletons = () => (
|
||||||
|
<>
|
||||||
|
{[0, 1].map((index) => (
|
||||||
|
<Card key={index} className="h-full overflow-hidden border-accent bg-accent/20">
|
||||||
|
<CardHeader className="space-y-3 p-4">
|
||||||
|
<Skeleton className="h-4 w-24 bg-accent" />
|
||||||
|
<Skeleton className="h-3 w-3/4 bg-accent" />
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent className="space-y-3 p-4 pt-0">
|
||||||
|
<Skeleton className="h-8 w-40 bg-accent" />
|
||||||
|
<Separator className="bg-accent" />
|
||||||
|
<Skeleton className="h-10 w-full bg-accent" />
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
))}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
|
||||||
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="order-1 group relative h-full overflow-hidden border-accent bg-accent/20 transition-all duration-200 hover:shadow-md">
|
{isGatewayConfigLoading ? renderGatewaySkeletons() : null}
|
||||||
<CardHeader className="space-y-1.5 p-4 pb-2">
|
|
||||||
<div className="flex items-center justify-between gap-3">
|
|
||||||
<CardTitle className="flex items-center gap-2 text-sm">Telegram</CardTitle>
|
|
||||||
</div>
|
|
||||||
<p className="text-xs text-muted-foreground">Pair Telegram with this search space.</p>
|
|
||||||
</CardHeader>
|
|
||||||
<CardContent className="space-y-3 p-4 pt-0">
|
|
||||||
<div className="flex flex-wrap gap-2">
|
|
||||||
{hasTelegramConnection ? null : (
|
|
||||||
<Button size="sm" onClick={() => startPairing("telegram")}>
|
|
||||||
Pair Telegram Chat
|
|
||||||
</Button>
|
|
||||||
)}
|
|
||||||
<Button
|
|
||||||
size="sm"
|
|
||||||
variant="secondary"
|
|
||||||
className={refreshButtonClassName}
|
|
||||||
onClick={() => refreshPlatform("telegram")}
|
|
||||||
disabled={isRefreshing("telegram")}
|
|
||||||
>
|
|
||||||
<RefreshCw className={refreshIconClassName("telegram")} />
|
|
||||||
Refresh
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{hasTelegramConnection ? null : renderPairingPanel("telegram")}
|
{!isGatewayConfigLoading && !hasEnabledGateway ? (
|
||||||
<Separator className="bg-accent" />
|
<Card className="col-span-full border-accent bg-accent/20">
|
||||||
{renderConnectionRows("telegram", "No Telegram chats connected yet.")}
|
<CardHeader className="space-y-1.5 p-4">
|
||||||
</CardContent>
|
<CardTitle className="text-sm">No messaging gateways enabled</CardTitle>
|
||||||
</Card>
|
</CardHeader>
|
||||||
|
</Card>
|
||||||
|
) : null}
|
||||||
|
|
||||||
|
{telegramGatewayEnabled ? (
|
||||||
|
<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">
|
||||||
|
<div className="flex items-center justify-between gap-3">
|
||||||
|
<CardTitle className="flex items-center gap-2 text-sm">Telegram</CardTitle>
|
||||||
|
</div>
|
||||||
|
<p className="text-xs text-muted-foreground">
|
||||||
|
Connect Telegram to chat with SurfSense.
|
||||||
|
</p>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent className="space-y-3 p-4 pt-0">
|
||||||
|
<div className="flex flex-wrap gap-2">
|
||||||
|
{hasTelegramConnection ? null : (
|
||||||
|
<Button size="sm" onClick={() => startPairing("telegram")}>
|
||||||
|
Pair Telegram Chat
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
<Button
|
||||||
|
size="sm"
|
||||||
|
variant="secondary"
|
||||||
|
className={refreshButtonClassName}
|
||||||
|
onClick={() => refreshPlatform("telegram")}
|
||||||
|
disabled={isRefreshing("telegram")}
|
||||||
|
>
|
||||||
|
<RefreshCw className={refreshIconClassName("telegram")} />
|
||||||
|
Refresh
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{hasTelegramConnection ? null : renderPairingPanel("telegram")}
|
||||||
|
<Separator className="bg-accent" />
|
||||||
|
{renderConnectionRows("telegram", "No Telegram chats connected yet.")}
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
) : null}
|
||||||
|
|
||||||
{slackGatewayEnabled ? (
|
{slackGatewayEnabled ? (
|
||||||
<Card className="order-4 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">
|
||||||
|
|
@ -437,8 +497,8 @@ export function MessagingChannelsContent() {
|
||||||
</div>
|
</div>
|
||||||
<p className="text-xs text-muted-foreground">
|
<p className="text-xs text-muted-foreground">
|
||||||
{whatsappMode === "baileys"
|
{whatsappMode === "baileys"
|
||||||
? "Use the WhatsApp bridge for your own WhatsApp chat. Other chats are ignored."
|
? 'Use "Message Yourself". Other chats are ignored.'
|
||||||
: "Pair this search space with WhatsApp Cloud API."}
|
: "Connect WhatsApp to chat with Surfsense."}
|
||||||
</p>
|
</p>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent className="space-y-3 p-4 pt-0">
|
<CardContent className="space-y-3 p-4 pt-0">
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue