added quick connect auth from jellyfin, still needs to have some more cleaning before push to prod
This commit is contained in:
+144
-2
@@ -27,6 +27,8 @@ from bot.conversation import ConversationStore
|
||||
from core.config import DEEPSEEK_API_KEY, get_config
|
||||
from core.graph import create_agent_graph
|
||||
from core.llm import create_client
|
||||
from core import auth_store
|
||||
from auth import list_auth_services, get_auth_service
|
||||
|
||||
logger = logging.getLogger("bot.discord")
|
||||
|
||||
@@ -36,6 +38,7 @@ logger = logging.getLogger("bot.discord")
|
||||
DISCORD_BOT_TOKEN = get_config("DISCORD_BOT_TOKEN") or ""
|
||||
DISCORD_MAX_HISTORY = int(get_config("DISCORD_MAX_HISTORY", "7"))
|
||||
DISCORD_DEFAULT_AGENT = get_config("DISCORD_DEFAULT_AGENT", "media-agent")
|
||||
BASE_URL = get_config("BASE_URL", "http://localhost:8000").rstrip("/")
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# LLM client shared by all agents (same as the REST API uses)
|
||||
@@ -138,6 +141,12 @@ class AgentBot(discord.Client):
|
||||
# |--------------------------------------------------------------|
|
||||
|
||||
user_id = message.author.id
|
||||
content = message.content.strip()
|
||||
|
||||
# |-- Bot commands — handled directly, never sent to the LLM --|
|
||||
if await self._handle_command(message, user_id, content):
|
||||
return
|
||||
# |--------------------------------------------------------------|
|
||||
|
||||
# Show typing indicator while the graph runs
|
||||
async with message.channel.typing():
|
||||
@@ -154,6 +163,140 @@ class AgentBot(discord.Client):
|
||||
"Please try again in a moment."
|
||||
)
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# Bot commands
|
||||
# ------------------------------------------------------------------
|
||||
|
||||
async def _handle_command(
|
||||
self, message: discord.Message, user_id: int, content: str
|
||||
) -> bool:
|
||||
"""Handle bot commands (/login, /logout). Returns True if handled."""
|
||||
lower = content.lower()
|
||||
|
||||
# --- /login [service] ---
|
||||
if lower.startswith("/login"):
|
||||
parts = content.split()
|
||||
service = parts[1].lower() if len(parts) > 1 else None
|
||||
|
||||
available = list_auth_services()
|
||||
if not available:
|
||||
await message.channel.send("No auth services are configured yet.")
|
||||
return True
|
||||
|
||||
if service is None:
|
||||
svc_list = ", ".join(available)
|
||||
await message.channel.send(
|
||||
f"Available services to link: **{svc_list}**\n"
|
||||
f"Use `/login <service>` — e.g. `/login jellyfin`"
|
||||
)
|
||||
return True
|
||||
|
||||
if service not in available:
|
||||
await message.channel.send(
|
||||
f"Unknown service '{service}'. Available: {', '.join(available)}"
|
||||
)
|
||||
return True
|
||||
|
||||
if auth_store.is_authenticated(user_id, service):
|
||||
svc_display = (get_auth_service(service) and get_auth_service(service).display_name) or service
|
||||
await message.channel.send(
|
||||
f"You're already linked to **{svc_display}**! "
|
||||
f"Use `/logout {service}` to unlink."
|
||||
)
|
||||
return True
|
||||
|
||||
# --- Quick Connect flow ---
|
||||
svc = get_auth_service(service)
|
||||
if svc is None:
|
||||
await message.channel.send(f"Unknown service: {service}")
|
||||
return True
|
||||
|
||||
await message.channel.send(f"🔑 Starting **{svc.display_name}** Quick Connect…")
|
||||
|
||||
qc_result = await svc.initiate_quick_connect()
|
||||
if qc_result is None:
|
||||
await message.channel.send(
|
||||
f"❌ Could not start Quick Connect for **{svc.display_name}**.\n"
|
||||
"Check that `JELLYFIN_URL` is configured and the server is reachable."
|
||||
)
|
||||
return True
|
||||
|
||||
await message.channel.send(
|
||||
f"Open **{svc.display_name}** → **Quick Connect** and enter this code:\n\n"
|
||||
f"**`{qc_result.code}`**\n\n"
|
||||
f"⏳ Waiting for you to approve…"
|
||||
)
|
||||
|
||||
# Poll for authorization
|
||||
async with message.channel.typing():
|
||||
for attempt in range(24): # 24 × 5s = 2 minutes
|
||||
await asyncio.sleep(5)
|
||||
status = await svc.poll_quick_connect(qc_result.secret)
|
||||
|
||||
if status == "Authorized":
|
||||
auth_result = await svc.authenticate_quick_connect(qc_result.secret)
|
||||
if auth_result.success:
|
||||
auth_store.store_auth(
|
||||
discord_user_id=user_id,
|
||||
service=service,
|
||||
external_user_id=auth_result.external_user_id or "",
|
||||
external_name=auth_result.external_name or "",
|
||||
credentials=auth_result.credentials,
|
||||
)
|
||||
await message.channel.send(
|
||||
f"✅ Linked to **{svc.display_name}** as "
|
||||
f"**{auth_result.external_name}**!"
|
||||
)
|
||||
else:
|
||||
await message.channel.send(
|
||||
f"❌ Authentication failed: "
|
||||
f"{auth_result.error_message or 'Unknown error'}"
|
||||
)
|
||||
return True
|
||||
|
||||
elif status == "Expired":
|
||||
await message.channel.send(
|
||||
"⌛ The Quick Connect code expired. "
|
||||
f"Use `/login {service}` to try again."
|
||||
)
|
||||
return True
|
||||
|
||||
# else: still "Active" — keep polling
|
||||
|
||||
await message.channel.send(
|
||||
"⌛ Timed out waiting for Quick Connect approval. "
|
||||
f"Use `/login {service}` to try again."
|
||||
)
|
||||
return True
|
||||
|
||||
# --- /logout [service] ---
|
||||
if lower.startswith("/logout"):
|
||||
parts = content.split()
|
||||
service = parts[1].lower() if len(parts) > 1 else None
|
||||
|
||||
if service is None:
|
||||
linked = auth_store.list_services(user_id)
|
||||
if not linked:
|
||||
await message.channel.send("You don't have any linked services.")
|
||||
else:
|
||||
svc_list = ", ".join(linked)
|
||||
await message.channel.send(
|
||||
f"Linked services: **{svc_list}**\n"
|
||||
f"Use `/logout <service>` to unlink."
|
||||
)
|
||||
return True
|
||||
|
||||
if not auth_store.is_authenticated(user_id, service):
|
||||
await message.channel.send(f"You're not linked to **{service}**.")
|
||||
return True
|
||||
|
||||
auth_store.revoke(user_id, service)
|
||||
svc_display = (get_auth_service(service) and get_auth_service(service).display_name) or service
|
||||
await message.channel.send(f"Unlinked from **{svc_display}**. Use `/login {service}` to re-link.")
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# Agent invocation
|
||||
# ------------------------------------------------------------------
|
||||
@@ -163,7 +306,6 @@ class AgentBot(discord.Client):
|
||||
reply, and return the assistant's final text."""
|
||||
|
||||
# 1. Pick agent — defaults to DISCORD_DEFAULT_AGENT env var.
|
||||
# Change DISCORD_DEFAULT_AGENT in .env to switch agents.
|
||||
agent_id = DISCORD_DEFAULT_AGENT
|
||||
|
||||
# 2. Build message list from stored history + new user message
|
||||
@@ -172,7 +314,7 @@ class AgentBot(discord.Client):
|
||||
|
||||
# 3. Run the LangGraph (tools execute inline if needed)
|
||||
graph = _get_graph(agent_id)
|
||||
state = {"messages": messages}
|
||||
state = {"messages": messages, "discord_user_id": user_id}
|
||||
result = await graph.ainvoke(state)
|
||||
|
||||
last_msg = result["messages"][-1]
|
||||
|
||||
Reference in New Issue
Block a user