feat: add Discord OAuth integration and connector routes

- Introduced Discord OAuth support with new environment variables for client ID, client secret, and redirect URI.
- Implemented Discord connector routes for OAuth flow, including authorization and callback handling.
- Enhanced Discord connector to support both OAuth-based authentication and legacy bot token usage.
- Updated Discord indexing logic to utilize OAuth credentials with auto-refresh capabilities.
- Removed outdated Discord UI components and adjusted frontend logic to reflect the new integration.
This commit is contained in:
Anish Sarkar 2026-01-05 14:21:39 +05:30
parent 1862732913
commit df23813f1c
13 changed files with 878 additions and 533 deletions

View file

@ -44,6 +44,12 @@ AIRTABLE_CLIENT_ID=your_airtable_client_id
AIRTABLE_CLIENT_SECRET=your_airtable_client_secret
AIRTABLE_REDIRECT_URI=http://localhost:8000/api/v1/auth/airtable/connector/callback
# Discord OAuth Configuration
DISCORD_CLIENT_ID=your_discord_client_id_here
DISCORD_CLIENT_SECRET=your_discord_client_secret_here
DISCORD_REDIRECT_URI=http://localhost:8000/api/v1/auth/discord/connector/callback
DISCORD_BOT_TOKEN=your_bot_token_from_developer_portal
# OAuth for Linear Connector
LINEAR_CLIENT_ID=your_linear_client_id
LINEAR_CLIENT_SECRET=your_linear_client_secret

View file

@ -105,6 +105,12 @@ class Config:
SLACK_CLIENT_SECRET = os.getenv("SLACK_CLIENT_SECRET")
SLACK_REDIRECT_URI = os.getenv("SLACK_REDIRECT_URI")
# Discord OAuth
DISCORD_CLIENT_ID = os.getenv("DISCORD_CLIENT_ID")
DISCORD_CLIENT_SECRET = os.getenv("DISCORD_CLIENT_SECRET")
DISCORD_REDIRECT_URI = os.getenv("DISCORD_REDIRECT_URI")
DISCORD_BOT_TOKEN = os.getenv("DISCORD_BOT_TOKEN")
# LLM instances are now managed per-user through the LLMConfig system
# Legacy environment variables removed in favor of user-specific configurations

View file

@ -3,7 +3,7 @@ Discord Connector
A module for interacting with Discord's HTTP API to retrieve guilds, channels, and message history.
Requires a Discord bot token.
Supports both direct bot token and OAuth-based authentication with token refresh.
"""
import asyncio
@ -12,6 +12,14 @@ import logging
import discord
from discord.ext import commands
from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy.future import select
from app.config import config
from app.db import SearchSourceConnector
from app.routes.discord_add_connector_route import refresh_discord_token
from app.schemas.discord_auth_credentials import DiscordAuthCredentialsBase
from app.utils.oauth_security import TokenEncryption
logger = logging.getLogger(__name__)
@ -19,12 +27,21 @@ logger = logging.getLogger(__name__)
class DiscordConnector(commands.Bot):
"""Class for retrieving guild, channel, and message history from Discord."""
def __init__(self, token: str | None = None):
def __init__(
self,
token: str | None = None,
session: AsyncSession | None = None,
connector_id: int | None = None,
credentials: DiscordAuthCredentialsBase | None = None,
):
"""
Initialize the DiscordConnector with a bot token.
Initialize the DiscordConnector with a bot token or OAuth credentials.
Args:
token (str): The Discord bot token.
token: Discord bot token (optional, for backward compatibility)
session: Database session for token refresh (optional)
connector_id: Connector ID for token refresh (optional)
credentials: Discord OAuth credentials (optional, will be loaded from DB if not provided)
"""
intents = discord.Intents.default()
intents.guilds = True # Required to fetch guilds and channels
@ -34,7 +51,14 @@ class DiscordConnector(commands.Bot):
super().__init__(
command_prefix="!", intents=intents
) # command_prefix is required but not strictly used here
self.token = token
self._session = session
self._connector_id = connector_id
self._credentials = credentials
# For backward compatibility, if token is provided directly, use it
if token:
self.token = token
else:
self.token = None
self._bot_task = None # Holds the async bot task
self._is_running = False # Flag to track if the bot is running
@ -57,12 +81,143 @@ class DiscordConnector(commands.Bot):
async def on_resumed():
logger.debug("Bot resumed connection to Discord gateway.")
async def _get_valid_token(self) -> str:
"""
Get valid Discord bot token, refreshing if needed.
Returns:
Valid bot token
Raises:
ValueError: If credentials are missing or invalid
Exception: If token refresh fails
"""
# If we have a direct token (backward compatibility), use it
if (
self.token
and self._session is None
and self._connector_id is None
and self._credentials is None
):
# This means it was initialized with a direct token, use it
return self.token
# Load credentials from DB if not provided
if self._credentials is None:
if not self._session or not self._connector_id:
raise ValueError(
"Cannot load credentials: session and connector_id required"
)
result = await self._session.execute(
select(SearchSourceConnector).filter(
SearchSourceConnector.id == self._connector_id
)
)
connector = result.scalars().first()
if not connector:
raise ValueError(f"Connector {self._connector_id} not found")
config_data = connector.config.copy()
# Decrypt credentials if they are encrypted
token_encrypted = config_data.get("_token_encrypted", False)
if token_encrypted and config.SECRET_KEY:
try:
token_encryption = TokenEncryption(config.SECRET_KEY)
# Decrypt sensitive fields
if config_data.get("bot_token"):
config_data["bot_token"] = token_encryption.decrypt_token(
config_data["bot_token"]
)
if config_data.get("refresh_token"):
config_data["refresh_token"] = token_encryption.decrypt_token(
config_data["refresh_token"]
)
logger.info(
f"Decrypted Discord credentials for connector {self._connector_id}"
)
except Exception as e:
logger.error(
f"Failed to decrypt Discord credentials for connector {self._connector_id}: {e!s}"
)
raise ValueError(
f"Failed to decrypt Discord credentials: {e!s}"
) from e
try:
self._credentials = DiscordAuthCredentialsBase.from_dict(config_data)
except Exception as e:
raise ValueError(f"Invalid Discord credentials: {e!s}") from e
# Check if token is expired and refreshable
if self._credentials.is_expired and self._credentials.is_refreshable:
try:
logger.info(
f"Discord token expired for connector {self._connector_id}, refreshing..."
)
# Get connector for refresh
result = await self._session.execute(
select(SearchSourceConnector).filter(
SearchSourceConnector.id == self._connector_id
)
)
connector = result.scalars().first()
if not connector:
raise RuntimeError(
f"Connector {self._connector_id} not found; cannot refresh token."
)
# Refresh token
connector = await refresh_discord_token(self._session, connector)
# Reload credentials after refresh
config_data = connector.config.copy()
token_encrypted = config_data.get("_token_encrypted", False)
if token_encrypted and config.SECRET_KEY:
token_encryption = TokenEncryption(config.SECRET_KEY)
if config_data.get("bot_token"):
config_data["bot_token"] = token_encryption.decrypt_token(
config_data["bot_token"]
)
if config_data.get("refresh_token"):
config_data["refresh_token"] = token_encryption.decrypt_token(
config_data["refresh_token"]
)
self._credentials = DiscordAuthCredentialsBase.from_dict(config_data)
logger.info(
f"Successfully refreshed Discord token for connector {self._connector_id}"
)
except Exception as e:
logger.error(
f"Failed to refresh Discord token for connector {self._connector_id}: {e!s}"
)
raise Exception(
f"Failed to refresh Discord OAuth credentials: {e!s}"
) from e
return self._credentials.bot_token
async def start_bot(self):
"""Starts the bot to connect to Discord."""
logger.info("Starting Discord bot...")
# Get valid token (with auto-refresh if using OAuth)
if not self.token:
raise ValueError("Discord bot token not set. Call set_token(token) first.")
# Try to get token from credentials
try:
self.token = await self._get_valid_token()
except ValueError as e:
raise ValueError(
f"Discord bot token not set. {e!s} Please authenticate via OAuth or provide a token."
) from e
try:
if self._is_running:
@ -107,7 +262,7 @@ class DiscordConnector(commands.Bot):
def set_token(self, token: str) -> None:
"""
Set the discord bot token.
Set the discord bot token (for backward compatibility).
Args:
token (str): The Discord bot token.

View file

@ -27,6 +27,7 @@ from .rbac_routes import router as rbac_router
from .search_source_connectors_routes import router as search_source_connectors_router
from .search_spaces_routes import router as search_spaces_router
from .slack_add_connector_route import router as slack_add_connector_router
from .discord_add_connector_route import router as discord_add_connector_router
router = APIRouter()
@ -46,6 +47,7 @@ router.include_router(linear_add_connector_router)
router.include_router(luma_add_connector_router)
router.include_router(notion_add_connector_router)
router.include_router(slack_add_connector_router)
router.include_router(discord_add_connector_router)
router.include_router(new_llm_config_router) # LLM configs with prompt configuration
router.include_router(logs_router)
router.include_router(circleback_webhook_router) # Circleback meeting webhooks

View file

@ -0,0 +1,509 @@
"""
Discord Connector OAuth Routes.
Handles OAuth 2.0 authentication flow for Discord connector.
"""
import logging
from datetime import UTC, datetime, timedelta
from uuid import UUID
import httpx
from fastapi import APIRouter, Depends, HTTPException, Request
from fastapi.responses import RedirectResponse
from pydantic import ValidationError
from sqlalchemy.exc import IntegrityError
from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy.future import select
from app.config import config
from app.db import (
SearchSourceConnector,
SearchSourceConnectorType,
User,
get_async_session,
)
from app.schemas.discord_auth_credentials import DiscordAuthCredentialsBase
from app.users import current_active_user
from app.utils.oauth_security import OAuthStateManager, TokenEncryption
logger = logging.getLogger(__name__)
router = APIRouter()
# Discord OAuth endpoints
AUTHORIZATION_URL = "https://discord.com/api/oauth2/authorize"
TOKEN_URL = "https://discord.com/api/oauth2/token"
# OAuth scopes for Discord (Bot Token)
SCOPES = [
"bot", # Basic bot scope
"guilds", # Access to guild information
"guilds.members.read", # Read member information
]
# Initialize security utilities
_state_manager = None
_token_encryption = None
def get_state_manager() -> OAuthStateManager:
"""Get or create OAuth state manager instance."""
global _state_manager
if _state_manager is None:
if not config.SECRET_KEY:
raise ValueError("SECRET_KEY must be set for OAuth security")
_state_manager = OAuthStateManager(config.SECRET_KEY)
return _state_manager
def get_token_encryption() -> TokenEncryption:
"""Get or create token encryption instance."""
global _token_encryption
if _token_encryption is None:
if not config.SECRET_KEY:
raise ValueError("SECRET_KEY must be set for token encryption")
_token_encryption = TokenEncryption(config.SECRET_KEY)
return _token_encryption
@router.get("/auth/discord/connector/add")
async def connect_discord(space_id: int, user: User = Depends(current_active_user)):
"""
Initiate Discord OAuth flow.
Args:
space_id: The search space ID
user: Current authenticated user
Returns:
Authorization URL for redirect
"""
try:
if not space_id:
raise HTTPException(status_code=400, detail="space_id is required")
if not config.DISCORD_CLIENT_ID:
raise HTTPException(status_code=500, detail="Discord OAuth not configured.")
if not config.DISCORD_BOT_TOKEN:
raise HTTPException(
status_code=500,
detail="Discord bot token not configured. Please set DISCORD_BOT_TOKEN in backend configuration.",
)
if not config.SECRET_KEY:
raise HTTPException(
status_code=500, detail="SECRET_KEY not configured for OAuth security."
)
# Generate secure state parameter with HMAC signature
state_manager = get_state_manager()
state_encoded = state_manager.generate_secure_state(space_id, user.id)
# Build authorization URL
from urllib.parse import urlencode
auth_params = {
"client_id": config.DISCORD_CLIENT_ID,
"scope": " ".join(SCOPES), # Discord uses space-separated scopes
"redirect_uri": config.DISCORD_REDIRECT_URI,
"response_type": "code",
"state": state_encoded,
}
auth_url = f"{AUTHORIZATION_URL}?{urlencode(auth_params)}"
logger.info(f"Generated Discord OAuth URL for user {user.id}, space {space_id}")
return {"auth_url": auth_url}
except Exception as e:
logger.error(f"Failed to initiate Discord OAuth: {e!s}", exc_info=True)
raise HTTPException(
status_code=500, detail=f"Failed to initiate Discord OAuth: {e!s}"
) from e
@router.get("/auth/discord/connector/callback")
async def discord_callback(
request: Request,
code: str | None = None,
error: str | None = None,
state: str | None = None,
session: AsyncSession = Depends(get_async_session),
):
"""
Handle Discord OAuth callback.
Args:
request: FastAPI request object
code: Authorization code from Discord (if user granted access)
error: Error code from Discord (if user denied access or error occurred)
state: State parameter containing user/space info
session: Database session
Returns:
Redirect response to frontend
"""
try:
# Handle OAuth errors (e.g., user denied access)
if error:
logger.warning(f"Discord OAuth error: {error}")
# Try to decode state to get space_id for redirect, but don't fail if it's invalid
space_id = None
if state:
try:
state_manager = get_state_manager()
data = state_manager.validate_state(state)
space_id = data.get("space_id")
except Exception:
# If state is invalid, we'll redirect without space_id
logger.warning("Failed to validate state in error handler")
# Redirect to frontend with error parameter
if space_id:
return RedirectResponse(
url=f"{config.NEXT_FRONTEND_URL}/dashboard/{space_id}/new-chat?modal=connectors&tab=all&error=discord_oauth_denied"
)
else:
return RedirectResponse(
url=f"{config.NEXT_FRONTEND_URL}/dashboard?error=discord_oauth_denied"
)
# Validate required parameters for successful flow
if not code:
raise HTTPException(status_code=400, detail="Missing authorization code")
if not state:
raise HTTPException(status_code=400, detail="Missing state parameter")
# Validate and decode state with signature verification
state_manager = get_state_manager()
try:
data = state_manager.validate_state(state)
except HTTPException:
raise
except Exception as e:
raise HTTPException(
status_code=400, detail=f"Invalid state parameter: {e!s}"
) from e
user_id = UUID(data["user_id"])
space_id = data["space_id"]
# Validate redirect URI (security: ensure it matches configured value)
if not config.DISCORD_REDIRECT_URI:
raise HTTPException(
status_code=500, detail="DISCORD_REDIRECT_URI not configured"
)
# Exchange authorization code for access token
token_data = {
"client_id": config.DISCORD_CLIENT_ID,
"client_secret": config.DISCORD_CLIENT_SECRET,
"grant_type": "authorization_code",
"code": code,
"redirect_uri": config.DISCORD_REDIRECT_URI,
}
async with httpx.AsyncClient() as client:
token_response = await client.post(
TOKEN_URL,
data=token_data,
headers={"Content-Type": "application/x-www-form-urlencoded"},
timeout=30.0,
)
if token_response.status_code != 200:
error_detail = token_response.text
try:
error_json = token_response.json()
error_detail = error_json.get("error_description", error_json.get("error", error_detail))
except Exception:
pass
raise HTTPException(
status_code=400, detail=f"Token exchange failed: {error_detail}"
)
token_json = token_response.json()
# Log OAuth response for debugging (without sensitive data)
logger.info(f"Discord OAuth response received. Keys: {list(token_json.keys())}")
# Discord OAuth with 'bot' scope returns access_token (user token), not bot token
# The bot token must come from backend config (DISCORD_BOT_TOKEN)
# OAuth is used to authorize bot installation to servers, not to get bot token
if not config.DISCORD_BOT_TOKEN:
raise HTTPException(
status_code=500,
detail="Discord bot token not configured. Please set DISCORD_BOT_TOKEN in backend configuration.",
)
# Use the bot token from backend config (not the OAuth access_token)
bot_token = config.DISCORD_BOT_TOKEN
# Extract OAuth access_token and refresh_token (for reference, not used for bot operations)
oauth_access_token = token_json.get("access_token")
refresh_token = token_json.get("refresh_token")
# Encrypt sensitive tokens before storing
token_encryption = get_token_encryption()
# Calculate expiration time (UTC, tz-aware)
expires_at = None
if token_json.get("expires_in"):
now_utc = datetime.now(UTC)
expires_at = now_utc + timedelta(seconds=int(token_json["expires_in"]))
# Extract guild info from OAuth response if available
guild_id = None
guild_name = None
if token_json.get("guild"):
guild_id = token_json["guild"].get("id")
guild_name = token_json["guild"].get("name")
# Store the bot token from config and OAuth metadata
connector_config = {
"bot_token": token_encryption.encrypt_token(bot_token), # Use bot token from config
"oauth_access_token": token_encryption.encrypt_token(oauth_access_token)
if oauth_access_token
else None, # Store OAuth token for reference
"refresh_token": token_encryption.encrypt_token(refresh_token)
if refresh_token
else None,
"token_type": token_json.get("token_type", "Bearer"),
"expires_in": token_json.get("expires_in"),
"expires_at": expires_at.isoformat() if expires_at else None,
"scope": token_json.get("scope"),
"guild_id": guild_id,
"guild_name": guild_name,
# Mark that tokens are encrypted for backward compatibility
"_token_encrypted": True,
}
# Check if connector already exists for this search space and user
existing_connector_result = await session.execute(
select(SearchSourceConnector).filter(
SearchSourceConnector.search_space_id == space_id,
SearchSourceConnector.user_id == user_id,
SearchSourceConnector.connector_type
== SearchSourceConnectorType.DISCORD_CONNECTOR,
)
)
existing_connector = existing_connector_result.scalars().first()
if existing_connector:
# Update existing connector
existing_connector.config = connector_config
existing_connector.name = "Discord Connector"
existing_connector.is_indexable = True
logger.info(
f"Updated existing Discord connector for user {user_id} in space {space_id}"
)
else:
# Create new connector
new_connector = SearchSourceConnector(
name="Discord Connector",
connector_type=SearchSourceConnectorType.DISCORD_CONNECTOR,
is_indexable=True,
config=connector_config,
search_space_id=space_id,
user_id=user_id,
)
session.add(new_connector)
logger.info(
f"Created new Discord connector for user {user_id} in space {space_id}"
)
try:
await session.commit()
logger.info(f"Successfully saved Discord connector for user {user_id}")
# Redirect to the frontend with success params
return RedirectResponse(
url=f"{config.NEXT_FRONTEND_URL}/dashboard/{space_id}/new-chat?modal=connectors&tab=all&success=true&connector=discord-connector"
)
except ValidationError as e:
await session.rollback()
raise HTTPException(
status_code=422, detail=f"Validation error: {e!s}"
) from e
except IntegrityError as e:
await session.rollback()
raise HTTPException(
status_code=409,
detail=f"Integrity error: A connector with this type already exists. {e!s}",
) from e
except Exception as e:
logger.error(f"Failed to create search source connector: {e!s}")
await session.rollback()
raise HTTPException(
status_code=500,
detail=f"Failed to create search source connector: {e!s}",
) from e
except HTTPException:
raise
except Exception as e:
logger.error(f"Failed to complete Discord OAuth: {e!s}", exc_info=True)
raise HTTPException(
status_code=500, detail=f"Failed to complete Discord OAuth: {e!s}"
) from e
async def refresh_discord_token(
session: AsyncSession, connector: SearchSourceConnector
) -> SearchSourceConnector:
"""
Refresh the Discord OAuth tokens for a connector.
Note: Bot tokens from config don't expire, but OAuth access tokens might.
This function refreshes OAuth tokens if needed, but always uses bot token from config.
Args:
session: Database session
connector: Discord connector to refresh
Returns:
Updated connector object
"""
try:
logger.info(f"Refreshing Discord OAuth tokens for connector {connector.id}")
# Bot token always comes from config, not from OAuth
if not config.DISCORD_BOT_TOKEN:
raise HTTPException(
status_code=500,
detail="Discord bot token not configured. Please set DISCORD_BOT_TOKEN in backend configuration.",
)
credentials = DiscordAuthCredentialsBase.from_dict(connector.config)
# Decrypt tokens if they are encrypted
token_encryption = get_token_encryption()
is_encrypted = connector.config.get("_token_encrypted", False)
refresh_token = credentials.refresh_token
if is_encrypted and refresh_token:
try:
refresh_token = token_encryption.decrypt_token(refresh_token)
except Exception as e:
logger.error(f"Failed to decrypt refresh token: {e!s}")
raise HTTPException(
status_code=500, detail="Failed to decrypt stored refresh token"
) from e
# If no refresh token, bot token from config is still valid (bot tokens don't expire)
# Just update the bot token from config in case it was changed
if not refresh_token:
logger.info(
f"No refresh token available for connector {connector.id}. Using bot token from config."
)
# Update bot token from config (in case it was changed)
credentials.bot_token = token_encryption.encrypt_token(config.DISCORD_BOT_TOKEN)
credentials_dict = credentials.to_dict()
credentials_dict["_token_encrypted"] = True
connector.config = credentials_dict
await session.commit()
await session.refresh(connector)
return connector
# Discord uses oauth2/token for token refresh with grant_type=refresh_token
refresh_data = {
"client_id": config.DISCORD_CLIENT_ID,
"client_secret": config.DISCORD_CLIENT_SECRET,
"grant_type": "refresh_token",
"refresh_token": refresh_token,
}
async with httpx.AsyncClient() as client:
token_response = await client.post(
TOKEN_URL,
data=refresh_data,
headers={"Content-Type": "application/x-www-form-urlencoded"},
timeout=30.0,
)
if token_response.status_code != 200:
error_detail = token_response.text
try:
error_json = token_response.json()
error_detail = error_json.get("error_description", error_json.get("error", error_detail))
except Exception:
pass
# If refresh fails, bot token from config is still valid
logger.warning(
f"OAuth token refresh failed for connector {connector.id}: {error_detail}. "
"Using bot token from config."
)
# Update bot token from config
credentials.bot_token = token_encryption.encrypt_token(config.DISCORD_BOT_TOKEN)
credentials.refresh_token = None # Clear invalid refresh token
credentials_dict = credentials.to_dict()
credentials_dict["_token_encrypted"] = True
connector.config = credentials_dict
await session.commit()
await session.refresh(connector)
return connector
token_json = token_response.json()
# Extract OAuth access token from refresh response (for reference)
oauth_access_token = token_json.get("access_token")
# Get new refresh token if provided (Discord may rotate refresh tokens)
new_refresh_token = token_json.get("refresh_token")
# Calculate expiration time (UTC, tz-aware)
expires_at = None
expires_in = token_json.get("expires_in")
if expires_in:
now_utc = datetime.now(UTC)
expires_at = now_utc + timedelta(seconds=int(expires_in))
# Always use bot token from config (bot tokens don't expire)
credentials.bot_token = token_encryption.encrypt_token(config.DISCORD_BOT_TOKEN)
# Update OAuth tokens if available
if oauth_access_token:
# Store OAuth access token for reference
connector.config["oauth_access_token"] = token_encryption.encrypt_token(
oauth_access_token
)
if new_refresh_token:
credentials.refresh_token = token_encryption.encrypt_token(
new_refresh_token
)
credentials.expires_in = expires_in
credentials.expires_at = expires_at
credentials.scope = token_json.get("scope")
# Preserve guild info if present
if not credentials.guild_id:
credentials.guild_id = connector.config.get("guild_id")
if not credentials.guild_name:
credentials.guild_name = connector.config.get("guild_name")
if not credentials.bot_user_id:
credentials.bot_user_id = connector.config.get("bot_user_id")
# Update connector config with encrypted tokens
credentials_dict = credentials.to_dict()
credentials_dict["_token_encrypted"] = True
connector.config = credentials_dict
await session.commit()
await session.refresh(connector)
logger.info(f"Successfully refreshed Discord OAuth tokens for connector {connector.id}")
return connector
except HTTPException:
raise
except Exception as e:
logger.error(
f"Failed to refresh Discord tokens for connector {connector.id}: {e!s}",
exc_info=True,
)
raise HTTPException(
status_code=500, detail=f"Failed to refresh Discord tokens: {e!s}"
) from e

View file

@ -0,0 +1,76 @@
from datetime import UTC, datetime
from pydantic import BaseModel, field_validator
class DiscordAuthCredentialsBase(BaseModel):
bot_token: str
refresh_token: str | None = None
token_type: str = "Bearer"
expires_in: int | None = None
expires_at: datetime | None = None
scope: str | None = None
bot_user_id: str | None = None
guild_id: str | None = None
guild_name: str | None = None
@property
def is_expired(self) -> bool:
"""Check if the credentials have expired."""
if self.expires_at is None:
return False # Long-lived token, treat as not expired
return self.expires_at <= datetime.now(UTC)
@property
def is_refreshable(self) -> bool:
"""Check if the credentials can be refreshed."""
return self.refresh_token is not None
def to_dict(self) -> dict:
"""Convert credentials to dictionary for storage."""
return {
"bot_token": self.bot_token,
"refresh_token": self.refresh_token,
"token_type": self.token_type,
"expires_in": self.expires_in,
"expires_at": self.expires_at.isoformat() if self.expires_at else None,
"scope": self.scope,
"bot_user_id": self.bot_user_id,
"guild_id": self.guild_id,
"guild_name": self.guild_name,
}
@classmethod
def from_dict(cls, data: dict) -> "DiscordAuthCredentialsBase":
"""Create credentials from dictionary."""
expires_at = None
if data.get("expires_at"):
expires_at = datetime.fromisoformat(data["expires_at"])
return cls(
bot_token=data.get("bot_token", ""),
refresh_token=data.get("refresh_token"),
token_type=data.get("token_type", "Bearer"),
expires_in=data.get("expires_in"),
expires_at=expires_at,
scope=data.get("scope"),
bot_user_id=data.get("bot_user_id"),
guild_id=data.get("guild_id"),
guild_name=data.get("guild_name"),
)
@field_validator("expires_at", mode="before")
@classmethod
def ensure_aware_utc(cls, v):
# Strings like "2025-08-26T14:46:57.367184"
if isinstance(v, str):
# add +00:00 if missing tz info
if v.endswith("Z"):
return datetime.fromisoformat(v.replace("Z", "+00:00"))
dt = datetime.fromisoformat(v)
return dt if dt.tzinfo else dt.replace(tzinfo=UTC)
# datetime objects
if isinstance(v, datetime):
return v if v.tzinfo else v.replace(tzinfo=UTC)
return v

View file

@ -8,6 +8,7 @@ from datetime import UTC, datetime, timedelta
from sqlalchemy.exc import SQLAlchemyError
from sqlalchemy.ext.asyncio import AsyncSession
from app.config import config
from app.connectors.discord_connector import DiscordConnector
from app.db import Document, DocumentType, SearchSourceConnectorType
from app.services.llm_service import get_user_long_context_llm
@ -69,6 +70,12 @@ async def index_discord_messages(
)
try:
# Normalize date parameters - handle 'undefined' strings from frontend
if start_date and (start_date.lower() == "undefined" or start_date.strip() == ""):
start_date = None
if end_date and (end_date.lower() == "undefined" or end_date.strip() == ""):
end_date = None
# Get the connector
await task_logger.log_task_progress(
log_entry,
@ -92,27 +99,54 @@ async def index_discord_messages(
f"Connector with ID {connector_id} not found or is not a Discord connector",
)
# Get the Discord token from the connector config
discord_token = connector.config.get("DISCORD_BOT_TOKEN")
if not discord_token:
await task_logger.log_task_failure(
log_entry,
f"Discord token not found in connector config for connector {connector_id}",
"Missing Discord token",
{"error_type": "MissingToken"},
)
return 0, "Discord token not found in connector config"
logger.info(f"Starting Discord indexing for connector {connector_id}")
# Initialize Discord client
# Initialize Discord client with OAuth credentials support
await task_logger.log_task_progress(
log_entry,
f"Initializing Discord client for connector {connector_id}",
{"stage": "client_initialization"},
)
discord_client = DiscordConnector(token=discord_token)
# Check if using OAuth (has bot_token in config) or legacy (has DISCORD_BOT_TOKEN)
has_oauth = connector.config.get("bot_token") is not None
has_legacy = connector.config.get("DISCORD_BOT_TOKEN") is not None
if has_oauth:
# Use OAuth credentials with auto-refresh
discord_client = DiscordConnector(
session=session, connector_id=connector_id
)
elif has_legacy:
# Backward compatibility: use legacy token format
discord_token = connector.config.get("DISCORD_BOT_TOKEN")
# Decrypt token if it's encrypted (legacy tokens might be encrypted)
token_encrypted = connector.config.get("_token_encrypted", False)
if token_encrypted and config.SECRET_KEY and discord_token:
try:
from app.utils.oauth_security import TokenEncryption
token_encryption = TokenEncryption(config.SECRET_KEY)
discord_token = token_encryption.decrypt_token(discord_token)
logger.info(
f"Decrypted legacy Discord token for connector {connector_id}"
)
except Exception as e:
logger.warning(
f"Failed to decrypt legacy Discord token for connector {connector_id}: {e!s}. "
"Trying to use token as-is (might be unencrypted)."
)
# Continue with token as-is - might be unencrypted legacy token
discord_client = DiscordConnector(token=discord_token)
else:
await task_logger.log_task_failure(
log_entry,
f"Discord credentials not found in connector config for connector {connector_id}",
"Missing Discord credentials",
{"error_type": "MissingCredentials"},
)
return 0, "Discord credentials not found in connector config"
# Calculate date range
if start_date is None or end_date is None:
@ -135,32 +169,63 @@ async def index_discord_messages(
if start_date is None:
start_date_iso = calculated_start_date.isoformat()
else:
# Convert YYYY-MM-DD to ISO format
# Validate and convert YYYY-MM-DD to ISO format
try:
start_date_iso = (
datetime.strptime(start_date, "%Y-%m-%d")
.replace(tzinfo=UTC)
.isoformat()
)
except ValueError as e:
logger.warning(
f"Invalid start_date format '{start_date}', using calculated start date: {e!s}"
)
start_date_iso = calculated_start_date.isoformat()
if end_date is None:
end_date_iso = calculated_end_date.isoformat()
else:
# Validate and convert YYYY-MM-DD to ISO format
try:
end_date_iso = (
datetime.strptime(end_date, "%Y-%m-%d")
.replace(tzinfo=UTC)
.isoformat()
)
except ValueError as e:
logger.warning(
f"Invalid end_date format '{end_date}', using calculated end date: {e!s}"
)
end_date_iso = calculated_end_date.isoformat()
else:
# Convert provided dates to ISO format for Discord API
try:
start_date_iso = (
datetime.strptime(start_date, "%Y-%m-%d")
.replace(tzinfo=UTC)
.isoformat()
)
if end_date is None:
end_date_iso = calculated_end_date.isoformat()
else:
# Convert YYYY-MM-DD to ISO format
end_date_iso = (
datetime.strptime(end_date, "%Y-%m-%d")
.replace(tzinfo=UTC)
.isoformat()
except ValueError as e:
await task_logger.log_task_failure(
log_entry,
f"Invalid start_date format: {start_date}",
f"Date parsing error: {e!s}",
{"error_type": "InvalidDateFormat", "start_date": start_date},
)
else:
# Convert provided dates to ISO format for Discord API
start_date_iso = (
datetime.strptime(start_date, "%Y-%m-%d")
.replace(tzinfo=UTC)
.isoformat()
)
end_date_iso = (
datetime.strptime(end_date, "%Y-%m-%d").replace(tzinfo=UTC).isoformat()
)
return 0, f"Invalid start_date format: {start_date}. Expected YYYY-MM-DD format."
try:
end_date_iso = (
datetime.strptime(end_date, "%Y-%m-%d").replace(tzinfo=UTC).isoformat()
)
except ValueError as e:
await task_logger.log_task_failure(
log_entry,
f"Invalid end_date format: {end_date}",
f"Date parsing error: {e!s}",
{"error_type": "InvalidDateFormat", "end_date": end_date},
)
return 0, f"Invalid end_date format: {end_date}. Expected YYYY-MM-DD format."
logger.info(
f"Indexing Discord messages from {start_date_iso} to {end_date_iso}"

View file

@ -537,7 +537,7 @@ def validate_connector_config(
)
},
},
"DISCORD_CONNECTOR": {"required": ["DISCORD_BOT_TOKEN"], "validators": {}},
# "DISCORD_CONNECTOR": {"required": ["DISCORD_BOT_TOKEN"], "validators": {}},
"JIRA_CONNECTOR": {
"required": ["JIRA_EMAIL", "JIRA_API_TOKEN", "JIRA_BASE_URL"],
"validators": {

View file

@ -1,409 +0,0 @@
"use client";
import { zodResolver } from "@hookform/resolvers/zod";
import { Info } from "lucide-react";
import type { FC } from "react";
import { useRef, useState } from "react";
import { useForm } from "react-hook-form";
import * as z from "zod";
import {
Accordion,
AccordionContent,
AccordionItem,
AccordionTrigger,
} from "@/components/ui/accordion";
import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert";
import {
Form,
FormControl,
FormDescription,
FormField,
FormItem,
FormLabel,
FormMessage,
} from "@/components/ui/form";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
import { Switch } from "@/components/ui/switch";
import { EnumConnectorName } from "@/contracts/enums/connector";
import { DateRangeSelector } from "../../components/date-range-selector";
import { getConnectorBenefits } from "../connector-benefits";
import type { ConnectFormProps } from "../index";
const discordConnectorFormSchema = z.object({
name: z.string().min(3, {
message: "Connector name must be at least 3 characters.",
}),
bot_token: z.string().min(10, {
message: "Discord Bot Token is required and must be valid.",
}),
});
type DiscordConnectorFormValues = z.infer<typeof discordConnectorFormSchema>;
export const DiscordConnectForm: FC<ConnectFormProps> = ({ onSubmit, isSubmitting }) => {
const isSubmittingRef = useRef(false);
const [startDate, setStartDate] = useState<Date | undefined>(undefined);
const [endDate, setEndDate] = useState<Date | undefined>(undefined);
const [periodicEnabled, setPeriodicEnabled] = useState(false);
const [frequencyMinutes, setFrequencyMinutes] = useState("1440");
const form = useForm<DiscordConnectorFormValues>({
resolver: zodResolver(discordConnectorFormSchema),
defaultValues: {
name: "Discord Connector",
bot_token: "",
},
});
const handleSubmit = async (values: DiscordConnectorFormValues) => {
// Prevent multiple submissions
if (isSubmittingRef.current || isSubmitting) {
return;
}
isSubmittingRef.current = true;
try {
await onSubmit({
name: values.name,
connector_type: EnumConnectorName.DISCORD_CONNECTOR,
config: {
DISCORD_BOT_TOKEN: values.bot_token,
},
is_indexable: true,
last_indexed_at: null,
periodic_indexing_enabled: periodicEnabled,
indexing_frequency_minutes: periodicEnabled ? parseInt(frequencyMinutes, 10) : null,
next_scheduled_at: null,
startDate,
endDate,
periodicEnabled,
frequencyMinutes,
});
} finally {
isSubmittingRef.current = false;
}
};
return (
<div className="space-y-6 pb-6">
<Alert className="bg-slate-400/5 dark:bg-white/5 border-slate-400/20 p-2 sm:p-3 flex items-center [&>svg]:relative [&>svg]:left-0 [&>svg]:top-0 [&>svg+div]:translate-y-0">
<Info className="h-3 w-3 sm:h-4 sm:w-4 shrink-0 ml-1" />
<div className="-ml-1">
<AlertTitle className="text-xs sm:text-sm">Bot Token Required</AlertTitle>
<AlertDescription className="text-[10px] sm:text-xs !pl-0">
You'll need a Discord Bot Token to use this connector. You can create one from{" "}
<a
href="https://discord.com/developers/applications"
target="_blank"
rel="noopener noreferrer"
className="font-medium underline underline-offset-4"
>
Discord Developer Portal
</a>
</AlertDescription>
</div>
</Alert>
<div className="rounded-xl border border-border bg-slate-400/5 dark:bg-white/5 p-3 sm:p-6 space-y-3 sm:space-y-4">
<Form {...form}>
<form
id="discord-connect-form"
onSubmit={form.handleSubmit(handleSubmit)}
className="space-y-4 sm:space-y-6"
>
<FormField
control={form.control}
name="name"
render={({ field }) => (
<FormItem>
<FormLabel className="text-xs sm:text-sm">Connector Name</FormLabel>
<FormControl>
<Input
placeholder="My Discord Connector"
className="h-8 sm:h-10 px-2 sm:px-3 text-xs sm:text-sm border-slate-400/20 focus-visible:border-slate-400/40"
disabled={isSubmitting}
{...field}
/>
</FormControl>
<FormDescription className="text-[10px] sm:text-xs">
A friendly name to identify this connector.
</FormDescription>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="bot_token"
render={({ field }) => (
<FormItem>
<FormLabel className="text-xs sm:text-sm">Discord Bot Token</FormLabel>
<FormControl>
<Input
type="password"
placeholder="Your Bot Token"
className="h-8 sm:h-10 px-2 sm:px-3 text-xs sm:text-sm border-slate-400/20 focus-visible:border-slate-400/40"
disabled={isSubmitting}
{...field}
/>
</FormControl>
<FormDescription className="text-[10px] sm:text-xs">
Your Discord Bot Token will be encrypted and stored securely.
</FormDescription>
<FormMessage />
</FormItem>
)}
/>
{/* Indexing Configuration */}
<div className="space-y-4 pt-4 border-t border-slate-400/20">
<h3 className="text-sm sm:text-base font-medium">Indexing Configuration</h3>
{/* Date Range Selector */}
<DateRangeSelector
startDate={startDate}
endDate={endDate}
onStartDateChange={setStartDate}
onEndDateChange={setEndDate}
/>
{/* Periodic Sync Config */}
<div className="rounded-xl bg-slate-400/5 dark:bg-white/5 p-3 sm:p-6">
<div className="flex items-center justify-between">
<div className="space-y-1">
<h3 className="font-medium text-sm sm:text-base">Enable Periodic Sync</h3>
<p className="text-xs sm:text-sm text-muted-foreground">
Automatically re-index at regular intervals
</p>
</div>
<Switch
checked={periodicEnabled}
onCheckedChange={setPeriodicEnabled}
disabled={isSubmitting}
/>
</div>
{periodicEnabled && (
<div className="mt-4 pt-4 border-t border-slate-400/20 space-y-3">
<div className="space-y-2">
<Label htmlFor="frequency" className="text-xs sm:text-sm">
Sync Frequency
</Label>
<Select
value={frequencyMinutes}
onValueChange={setFrequencyMinutes}
disabled={isSubmitting}
>
<SelectTrigger
id="frequency"
className="w-full bg-slate-400/5 dark:bg-slate-400/5 border-slate-400/20 text-xs sm:text-sm"
>
<SelectValue placeholder="Select frequency" />
</SelectTrigger>
<SelectContent className="z-[100]">
<SelectItem value="5" className="text-xs sm:text-sm">
Every 5 minutes
</SelectItem>
<SelectItem value="15" className="text-xs sm:text-sm">
Every 15 minutes
</SelectItem>
<SelectItem value="60" className="text-xs sm:text-sm">
Every hour
</SelectItem>
<SelectItem value="360" className="text-xs sm:text-sm">
Every 6 hours
</SelectItem>
<SelectItem value="720" className="text-xs sm:text-sm">
Every 12 hours
</SelectItem>
<SelectItem value="1440" className="text-xs sm:text-sm">
Daily
</SelectItem>
<SelectItem value="10080" className="text-xs sm:text-sm">
Weekly
</SelectItem>
</SelectContent>
</Select>
</div>
</div>
)}
</div>
</div>
</form>
</Form>
</div>
{/* What you get section */}
{getConnectorBenefits(EnumConnectorName.DISCORD_CONNECTOR) && (
<div className="rounded-xl border border-border bg-slate-400/5 dark:bg-white/5 px-3 sm:px-6 py-4 space-y-2">
<h4 className="text-xs sm:text-sm font-medium">What you get with Discord integration:</h4>
<ul className="list-disc pl-5 text-[10px] sm:text-xs text-muted-foreground space-y-1">
{getConnectorBenefits(EnumConnectorName.DISCORD_CONNECTOR)?.map((benefit) => (
<li key={benefit}>{benefit}</li>
))}
</ul>
</div>
)}
{/* Documentation Section */}
<Accordion
type="single"
collapsible
className="w-full border border-border rounded-xl bg-slate-400/5 dark:bg-white/5"
>
<AccordionItem value="documentation" className="border-0">
<AccordionTrigger className="text-sm sm:text-base font-medium px-3 sm:px-6 no-underline hover:no-underline">
Documentation
</AccordionTrigger>
<AccordionContent className="px-3 sm:px-6 pb-3 sm:pb-6 space-y-6">
<div>
<h3 className="text-sm sm:text-base font-semibold mb-2">How it works</h3>
<p className="text-[10px] sm:text-xs text-muted-foreground">
The Discord connector uses the Discord API to fetch messages from all accessible
channels that the bot token has access to within a server.
</p>
<ul className="mt-2 list-disc pl-5 text-[10px] sm:text-xs text-muted-foreground space-y-1">
<li>
For follow up indexing runs, the connector retrieves messages that have been
updated since the last indexing attempt.
</li>
<li>
Indexing is configured to run periodically, so updates should appear in your
search results within minutes.
</li>
</ul>
</div>
<div className="space-y-4">
<div>
<h3 className="text-sm sm:text-base font-semibold mb-2">Authorization</h3>
<Alert className="bg-slate-400/5 dark:bg-white/5 border-slate-400/20 mb-4">
<Info className="h-3 w-3 sm:h-4 sm:w-4" />
<AlertTitle className="text-[10px] sm:text-xs">Bot Token Required</AlertTitle>
<AlertDescription className="text-[9px] sm:text-[10px]">
You need to create a Discord application and bot to get a bot token. The bot
needs read access to channels and messages.
</AlertDescription>
</Alert>
<div className="space-y-4 sm:space-y-6">
<div>
<h4 className="text-[10px] sm:text-xs font-medium mb-2">
Step 1: Create a Discord Application
</h4>
<ol className="list-decimal pl-5 space-y-2 text-[10px] sm:text-xs text-muted-foreground">
<li>
Go to{" "}
<a
href="https://discord.com/developers/applications"
target="_blank"
rel="noopener noreferrer"
className="font-medium underline underline-offset-4"
>
https://discord.com/developers/applications
</a>
</li>
<li>
Click <strong>New Application</strong>
</li>
<li>
Enter an application name and click <strong>Create</strong>
</li>
</ol>
</div>
<div>
<h4 className="text-[10px] sm:text-xs font-medium mb-2">
Step 2: Create a Bot
</h4>
<ol className="list-decimal pl-5 space-y-2 text-[10px] sm:text-xs text-muted-foreground">
<li>
Navigate to <strong>Bot</strong> in the sidebar
</li>
<li>
Click <strong>Add Bot</strong> and confirm
</li>
<li>
Under <strong>Privileged Gateway Intents</strong>, enable:
<ul className="list-disc pl-5 mt-1 space-y-1">
<li>
<code className="bg-muted px-1 py-0.5 rounded">
MESSAGE CONTENT INTENT
</code>{" "}
- Required to read message content
</li>
</ul>
</li>
</ol>
</div>
<div>
<h4 className="text-[10px] sm:text-xs font-medium mb-2">
Step 3: Get Bot Token and Invite Bot
</h4>
<ol className="list-decimal pl-5 space-y-2 text-[10px] sm:text-xs text-muted-foreground">
<li>
Under <strong>Token</strong>, click <strong>Reset Token</strong> and copy
the token
</li>
<li>
Navigate to <strong>OAuth2 URL Generator</strong>
</li>
<li>
Select <strong>bot</strong> scope and <strong>Read Messages</strong>{" "}
permission
</li>
<li>Copy the generated URL and open it in your browser</li>
<li>Select your server and authorize the bot</li>
</ol>
</div>
</div>
</div>
</div>
<div className="space-y-4">
<div>
<h3 className="text-sm sm:text-base font-semibold mb-2">Indexing</h3>
<ol className="list-decimal pl-5 space-y-2 text-[10px] sm:text-xs text-muted-foreground mb-4">
<li>
Navigate to the Connector Dashboard and select the <strong>Discord</strong>{" "}
Connector.
</li>
<li>
Place the <strong>Bot Token</strong> in the form field.
</li>
<li>
Click <strong>Connect</strong> to establish the connection.
</li>
<li>Once connected, your Discord messages will be indexed automatically.</li>
</ol>
<Alert className="bg-slate-400/5 dark:bg-white/5 border-slate-400/20">
<Info className="h-3 w-3 sm:h-4 sm:w-4" />
<AlertTitle className="text-[10px] sm:text-xs">What Gets Indexed</AlertTitle>
<AlertDescription className="text-[9px] sm:text-[10px]">
<p className="mb-2">The Discord connector indexes the following data:</p>
<ul className="list-disc pl-5 space-y-1">
<li>Messages from all accessible channels</li>
<li>Direct messages (if bot has access)</li>
<li>Message timestamps and metadata</li>
<li>Thread replies and conversations</li>
</ul>
</AlertDescription>
</Alert>
</div>
</div>
</AccordionContent>
</AccordionItem>
</Accordion>
</div>
);
};

View file

@ -4,7 +4,6 @@ import { BookStackConnectForm } from "./components/bookstack-connect-form";
import { CirclebackConnectForm } from "./components/circleback-connect-form";
import { ClickUpConnectForm } from "./components/clickup-connect-form";
import { ConfluenceConnectForm } from "./components/confluence-connect-form";
import { DiscordConnectForm } from "./components/discord-connect-form";
import { ElasticsearchConnectForm } from "./components/elasticsearch-connect-form";
import { GithubConnectForm } from "./components/github-connect-form";
import { JiraConnectForm } from "./components/jira-connect-form";
@ -50,8 +49,6 @@ export function getConnectFormComponent(connectorType: string): ConnectFormCompo
return BaiduSearchApiConnectForm;
case "ELASTICSEARCH_CONNECTOR":
return ElasticsearchConnectForm;
case "DISCORD_CONNECTOR":
return DiscordConnectForm;
case "CONFLUENCE_CONNECTOR":
return ConfluenceConnectForm;
case "BOOKSTACK_CONNECTOR":

View file

@ -1,88 +1,26 @@
"use client";
import { KeyRound } from "lucide-react";
import { Info } from "lucide-react";
import type { FC } from "react";
import { useEffect, useState } from "react";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import type { ConnectorConfigProps } from "../index";
export interface DiscordConfigProps extends ConnectorConfigProps {
onNameChange?: (name: string) => void;
}
export const DiscordConfig: FC<DiscordConfigProps> = ({
connector,
onConfigChange,
onNameChange,
}) => {
const [botToken, setBotToken] = useState<string>(
(connector.config?.DISCORD_BOT_TOKEN as string) || ""
);
const [name, setName] = useState<string>(connector.name || "");
// Update bot token and name when connector changes
useEffect(() => {
const token = (connector.config?.DISCORD_BOT_TOKEN as string) || "";
setBotToken(token);
setName(connector.name || "");
}, [connector.config, connector.name]);
const handleBotTokenChange = (value: string) => {
setBotToken(value);
if (onConfigChange) {
onConfigChange({
...connector.config,
DISCORD_BOT_TOKEN: value,
});
}
};
const handleNameChange = (value: string) => {
setName(value);
if (onNameChange) {
onNameChange(value);
}
};
export const DiscordConfig: FC<DiscordConfigProps> = () => {
return (
<div className="space-y-6">
{/* Connector Name */}
<div className="rounded-xl border border-border bg-slate-400/5 dark:bg-white/5 p-3 sm:p-6 space-y-3 sm:space-y-4">
<div className="space-y-2">
<Label className="text-xs sm:text-sm">Connector Name</Label>
<Input
value={name}
onChange={(e) => handleNameChange(e.target.value)}
placeholder="My Discord Connector"
className="border-slate-400/20 focus-visible:border-slate-400/40"
/>
<p className="text-[10px] sm:text-xs text-muted-foreground">
A friendly name to identify this connector.
</p>
<div className="rounded-xl border border-border bg-primary/5 p-4 flex items-start gap-3">
<div className="flex h-8 w-8 items-center justify-center rounded-lg bg-primary/10 shrink-0 mt-0.5">
<Info className="size-4" />
</div>
</div>
{/* Configuration */}
<div className="rounded-xl border border-border bg-slate-400/5 dark:bg-white/5 p-3 sm:p-6 space-y-3 sm:space-y-4">
<div className="space-y-1 sm:space-y-2">
<h3 className="font-medium text-sm sm:text-base">Configuration</h3>
</div>
<div className="space-y-2">
<Label className="flex items-center gap-2 text-xs sm:text-sm">
<KeyRound className="h-4 w-4" />
Discord Bot Token
</Label>
<Input
type="password"
value={botToken}
onChange={(e) => handleBotTokenChange(e.target.value)}
placeholder="Your Bot Token"
className="border-slate-400/20 focus-visible:border-slate-400/40"
/>
<p className="text-[10px] sm:text-xs text-muted-foreground">
Update your Discord Bot Token if needed.
<div className="text-xs sm:text-sm">
<p className="font-medium text-xs sm:text-sm">Add Bot to Servers</p>
<p className="text-muted-foreground mt-1 text-[10px] sm:text-sm">
Before indexing, make sure the Discord bot has been added to the servers (guilds) you want to
index. The bot can only access messages from servers it's been added to. Use the OAuth
authorization flow to add the bot to your servers.
</p>
</div>
</div>

View file

@ -52,7 +52,6 @@ export const ConnectorConnectView: FC<ConnectorConnectViewProps> = ({
LINKUP_API: "linkup-api-connect-form",
BAIDU_SEARCH_API: "baidu-search-api-connect-form",
ELASTICSEARCH_CONNECTOR: "elasticsearch-connect-form",
DISCORD_CONNECTOR: "discord-connect-form",
CONFLUENCE_CONNECTOR: "confluence-connect-form",
BOOKSTACK_CONNECTOR: "bookstack-connect-form",
GITHUB_CONNECTOR: "github-connect-form",

View file

@ -51,6 +51,13 @@ export const OAUTH_CONNECTORS = [
connectorType: EnumConnectorName.SLACK_CONNECTOR,
authEndpoint: "/api/v1/auth/slack/connector/add/",
},
{
id: "discord-connector",
title: "Discord",
description: "Search Discord messages",
connectorType: EnumConnectorName.DISCORD_CONNECTOR,
authEndpoint: "/api/v1/auth/discord/connector/add/",
},
] as const;
// Content Sources (tools that extract and import content from external sources)
@ -71,12 +78,6 @@ export const CRAWLERS = [
// Non-OAuth Connectors (redirect to old connector config pages)
export const OTHER_CONNECTORS = [
{
id: "discord-connector",
title: "Discord",
description: "Search Discord messages",
connectorType: EnumConnectorName.DISCORD_CONNECTOR,
},
{
id: "confluence-connector",
title: "Confluence",