mirror of
https://github.com/Significant-Gravitas/AutoGPT.git
synced 2026-04-08 03:00:28 -04:00
Compare commits
13 Commits
feat/keep-
...
feat/platf
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
77ebcfe55d | ||
|
|
db029f0b49 | ||
|
|
a268291585 | ||
|
|
9caca6d899 | ||
|
|
481f704317 | ||
|
|
781224f8d5 | ||
|
|
8a91bd8a99 | ||
|
|
7bf10c605a | ||
|
|
7778de1d7b | ||
|
|
015a626379 | ||
|
|
be6501f10e | ||
|
|
32f6ef0a45 | ||
|
|
c7d5c1c844 |
@@ -0,0 +1 @@
|
||||
# Platform bot linking API
|
||||
@@ -0,0 +1,35 @@
|
||||
"""Bot API key authentication for platform linking endpoints."""
|
||||
|
||||
import hmac
|
||||
import os
|
||||
|
||||
from fastapi import HTTPException, Request
|
||||
|
||||
from backend.util.settings import Settings
|
||||
|
||||
|
||||
async def get_bot_api_key(request: Request) -> str | None:
|
||||
"""Extract the bot API key from the X-Bot-API-Key header."""
|
||||
return request.headers.get("x-bot-api-key")
|
||||
|
||||
|
||||
def check_bot_api_key(api_key: str | None) -> None:
|
||||
"""Validate the bot API key. Uses constant-time comparison.
|
||||
|
||||
Reads the key from env on each call so rotated secrets take effect
|
||||
without restarting the process.
|
||||
"""
|
||||
configured_key = os.getenv("PLATFORM_BOT_API_KEY", "")
|
||||
|
||||
if not configured_key:
|
||||
settings = Settings()
|
||||
if settings.config.enable_auth:
|
||||
raise HTTPException(
|
||||
status_code=503,
|
||||
detail="Bot API key not configured.",
|
||||
)
|
||||
# Auth disabled (local dev) — allow without key
|
||||
return
|
||||
|
||||
if not api_key or not hmac.compare_digest(api_key, configured_key):
|
||||
raise HTTPException(status_code=401, detail="Invalid bot API key.")
|
||||
@@ -0,0 +1,170 @@
|
||||
"""
|
||||
Bot Chat Proxy endpoints.
|
||||
|
||||
Allows the bot service to send messages to CoPilot on behalf of
|
||||
linked users, authenticated via bot API key.
|
||||
"""
|
||||
|
||||
import asyncio
|
||||
import logging
|
||||
from uuid import uuid4
|
||||
|
||||
from fastapi import APIRouter, Depends, HTTPException
|
||||
from fastapi.responses import StreamingResponse
|
||||
from prisma.models import PlatformLink
|
||||
|
||||
from backend.copilot import stream_registry
|
||||
from backend.copilot.executor.utils import enqueue_copilot_turn
|
||||
from backend.copilot.model import (
|
||||
ChatMessage,
|
||||
append_and_save_message,
|
||||
create_chat_session,
|
||||
get_chat_session,
|
||||
)
|
||||
from backend.copilot.response_model import StreamFinish
|
||||
|
||||
from .auth import check_bot_api_key, get_bot_api_key
|
||||
from .models import BotChatRequest, BotChatSessionResponse
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
|
||||
@router.post(
|
||||
"/chat/session",
|
||||
response_model=BotChatSessionResponse,
|
||||
summary="Create a CoPilot session for a linked user (bot-facing)",
|
||||
)
|
||||
async def bot_create_session(
|
||||
request: BotChatRequest,
|
||||
x_bot_api_key: str | None = Depends(get_bot_api_key),
|
||||
) -> BotChatSessionResponse:
|
||||
"""Creates a new CoPilot chat session on behalf of a linked user."""
|
||||
check_bot_api_key(x_bot_api_key)
|
||||
|
||||
link = await PlatformLink.prisma().find_first(where={"userId": request.user_id})
|
||||
if not link:
|
||||
raise HTTPException(status_code=404, detail="User has no platform links.")
|
||||
|
||||
session = await create_chat_session(request.user_id)
|
||||
|
||||
return BotChatSessionResponse(session_id=session.session_id)
|
||||
|
||||
|
||||
@router.post(
|
||||
"/chat/stream",
|
||||
summary="Stream a CoPilot response for a linked user (bot-facing)",
|
||||
)
|
||||
async def bot_chat_stream(
|
||||
request: BotChatRequest,
|
||||
x_bot_api_key: str | None = Depends(get_bot_api_key),
|
||||
):
|
||||
"""
|
||||
Send a message to CoPilot on behalf of a linked user and stream
|
||||
the response back as Server-Sent Events.
|
||||
|
||||
The bot authenticates with its API key — no user JWT needed.
|
||||
"""
|
||||
check_bot_api_key(x_bot_api_key)
|
||||
|
||||
user_id = request.user_id
|
||||
|
||||
# Verify user has a platform link
|
||||
link = await PlatformLink.prisma().find_first(where={"userId": user_id})
|
||||
if not link:
|
||||
raise HTTPException(status_code=404, detail="User has no platform links.")
|
||||
|
||||
# Get or create session
|
||||
session_id = request.session_id
|
||||
if session_id:
|
||||
session = await get_chat_session(session_id, user_id)
|
||||
if not session:
|
||||
raise HTTPException(status_code=404, detail="Session not found.")
|
||||
else:
|
||||
session = await create_chat_session(user_id)
|
||||
session_id = session.session_id
|
||||
|
||||
# Save user message
|
||||
message = ChatMessage(role="user", content=request.message)
|
||||
await append_and_save_message(session_id, message)
|
||||
|
||||
# Create a turn and enqueue
|
||||
turn_id = str(uuid4())
|
||||
|
||||
await stream_registry.create_session(
|
||||
session_id=session_id,
|
||||
user_id=user_id,
|
||||
tool_call_id="chat_stream",
|
||||
tool_name="chat",
|
||||
turn_id=turn_id,
|
||||
)
|
||||
|
||||
subscribe_from_id = "0-0"
|
||||
|
||||
await enqueue_copilot_turn(
|
||||
session_id=session_id,
|
||||
user_id=user_id,
|
||||
message=request.message,
|
||||
turn_id=turn_id,
|
||||
is_user_message=True,
|
||||
)
|
||||
|
||||
logger.info(
|
||||
"Bot chat: user ...%s, session %s, turn %s",
|
||||
user_id[-8:],
|
||||
session_id,
|
||||
turn_id,
|
||||
)
|
||||
|
||||
async def event_generator():
|
||||
subscriber_queue = None
|
||||
try:
|
||||
subscriber_queue = await stream_registry.subscribe_to_session(
|
||||
session_id=session_id,
|
||||
user_id=user_id,
|
||||
last_message_id=subscribe_from_id,
|
||||
)
|
||||
|
||||
if subscriber_queue is None:
|
||||
yield StreamFinish().to_sse()
|
||||
yield "data: [DONE]\n\n"
|
||||
return
|
||||
|
||||
while True:
|
||||
try:
|
||||
chunk = await asyncio.wait_for(subscriber_queue.get(), timeout=30.0)
|
||||
|
||||
if isinstance(chunk, str):
|
||||
yield chunk
|
||||
else:
|
||||
yield chunk.to_sse()
|
||||
|
||||
if isinstance(chunk, StreamFinish) or (
|
||||
isinstance(chunk, str) and "[DONE]" in chunk
|
||||
):
|
||||
break
|
||||
|
||||
except asyncio.TimeoutError:
|
||||
yield ": keepalive\n\n"
|
||||
|
||||
except Exception:
|
||||
logger.exception("Bot chat stream error for session %s", session_id)
|
||||
yield 'data: {"type": "error", "content": "Stream error"}\n\n'
|
||||
yield "data: [DONE]\n\n"
|
||||
finally:
|
||||
if subscriber_queue is not None:
|
||||
await stream_registry.unsubscribe_from_session(
|
||||
session_id=session_id,
|
||||
subscriber_queue=subscriber_queue,
|
||||
)
|
||||
|
||||
return StreamingResponse(
|
||||
event_generator(),
|
||||
media_type="text/event-stream",
|
||||
headers={
|
||||
"Cache-Control": "no-cache",
|
||||
"Connection": "keep-alive",
|
||||
"X-Session-Id": session_id,
|
||||
},
|
||||
)
|
||||
@@ -0,0 +1,107 @@
|
||||
"""Pydantic models for the platform bot linking API."""
|
||||
|
||||
from datetime import datetime
|
||||
from enum import Enum
|
||||
from typing import Literal
|
||||
|
||||
from pydantic import BaseModel, Field
|
||||
|
||||
|
||||
class Platform(str, Enum):
|
||||
"""Supported platform types (mirrors Prisma PlatformType)."""
|
||||
|
||||
DISCORD = "DISCORD"
|
||||
TELEGRAM = "TELEGRAM"
|
||||
SLACK = "SLACK"
|
||||
TEAMS = "TEAMS"
|
||||
WHATSAPP = "WHATSAPP"
|
||||
GITHUB = "GITHUB"
|
||||
LINEAR = "LINEAR"
|
||||
|
||||
|
||||
# ── Request Models ─────────────────────────────────────────────────────
|
||||
|
||||
|
||||
class CreateLinkTokenRequest(BaseModel):
|
||||
"""Request from the bot service to create a linking token."""
|
||||
|
||||
platform: Platform = Field(description="Platform name")
|
||||
platform_user_id: str = Field(
|
||||
description="The user's ID on the platform",
|
||||
min_length=1,
|
||||
max_length=255,
|
||||
)
|
||||
platform_username: str | None = Field(
|
||||
default=None,
|
||||
description="Display name (best effort)",
|
||||
max_length=255,
|
||||
)
|
||||
channel_id: str | None = Field(
|
||||
default=None,
|
||||
description="Channel ID for sending confirmation back",
|
||||
max_length=255,
|
||||
)
|
||||
|
||||
|
||||
class ResolveRequest(BaseModel):
|
||||
"""Resolve a platform identity to an AutoGPT user."""
|
||||
|
||||
platform: Platform
|
||||
platform_user_id: str = Field(min_length=1, max_length=255)
|
||||
|
||||
|
||||
class BotChatRequest(BaseModel):
|
||||
"""Request from the bot to chat as a linked user."""
|
||||
|
||||
user_id: str = Field(description="The linked AutoGPT user ID")
|
||||
message: str = Field(
|
||||
description="The user's message", min_length=1, max_length=32000
|
||||
)
|
||||
session_id: str | None = Field(
|
||||
default=None,
|
||||
description="Existing chat session ID. If omitted, a new session is created.",
|
||||
)
|
||||
|
||||
|
||||
# ── Response Models ────────────────────────────────────────────────────
|
||||
|
||||
|
||||
class LinkTokenResponse(BaseModel):
|
||||
token: str
|
||||
expires_at: datetime
|
||||
link_url: str
|
||||
|
||||
|
||||
class LinkTokenStatusResponse(BaseModel):
|
||||
status: Literal["pending", "linked", "expired"]
|
||||
user_id: str | None = None
|
||||
|
||||
|
||||
class ResolveResponse(BaseModel):
|
||||
linked: bool
|
||||
user_id: str | None = None
|
||||
|
||||
|
||||
class PlatformLinkInfo(BaseModel):
|
||||
id: str
|
||||
platform: str
|
||||
platform_user_id: str
|
||||
platform_username: str | None
|
||||
linked_at: datetime
|
||||
|
||||
|
||||
class ConfirmLinkResponse(BaseModel):
|
||||
success: bool
|
||||
platform: str
|
||||
platform_user_id: str
|
||||
platform_username: str | None
|
||||
|
||||
|
||||
class DeleteLinkResponse(BaseModel):
|
||||
success: bool
|
||||
|
||||
|
||||
class BotChatSessionResponse(BaseModel):
|
||||
"""Returned when creating a new session via the bot proxy."""
|
||||
|
||||
session_id: str
|
||||
@@ -0,0 +1,340 @@
|
||||
"""
|
||||
Platform Bot Linking API routes.
|
||||
|
||||
Enables linking external chat platform identities (Discord, Telegram, Slack, etc.)
|
||||
to AutoGPT user accounts. Used by the multi-platform CoPilot bot.
|
||||
|
||||
Flow:
|
||||
1. Bot calls POST /api/platform-linking/tokens to create a link token
|
||||
for an unlinked platform user.
|
||||
2. Bot sends the user a link: {frontend}/link/{token}
|
||||
3. User clicks the link, logs in to AutoGPT, and the frontend calls
|
||||
POST /api/platform-linking/tokens/{token}/confirm to complete the link.
|
||||
4. Bot can poll GET /api/platform-linking/tokens/{token}/status or just
|
||||
check on next message via GET /api/platform-linking/resolve.
|
||||
"""
|
||||
|
||||
import logging
|
||||
import os
|
||||
import secrets
|
||||
from datetime import datetime, timedelta, timezone
|
||||
from typing import Annotated
|
||||
|
||||
from autogpt_libs import auth
|
||||
from fastapi import APIRouter, Depends, HTTPException, Path, Security
|
||||
from prisma.models import PlatformLink, PlatformLinkToken
|
||||
|
||||
from .auth import check_bot_api_key, get_bot_api_key
|
||||
from .models import (
|
||||
ConfirmLinkResponse,
|
||||
CreateLinkTokenRequest,
|
||||
DeleteLinkResponse,
|
||||
LinkTokenResponse,
|
||||
LinkTokenStatusResponse,
|
||||
PlatformLinkInfo,
|
||||
ResolveRequest,
|
||||
ResolveResponse,
|
||||
)
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
LINK_TOKEN_EXPIRY_MINUTES = 30
|
||||
|
||||
# Path parameter with validation for link tokens
|
||||
TokenPath = Annotated[
|
||||
str,
|
||||
Path(max_length=64, pattern=r"^[A-Za-z0-9_-]+$"),
|
||||
]
|
||||
|
||||
|
||||
# ── Bot-facing endpoints (API key auth) ───────────────────────────────
|
||||
|
||||
|
||||
@router.post(
|
||||
"/tokens",
|
||||
response_model=LinkTokenResponse,
|
||||
summary="Create a link token for an unlinked platform user",
|
||||
)
|
||||
async def create_link_token(
|
||||
request: CreateLinkTokenRequest,
|
||||
x_bot_api_key: str | None = Depends(get_bot_api_key),
|
||||
) -> LinkTokenResponse:
|
||||
"""
|
||||
Called by the bot service when it encounters an unlinked user.
|
||||
Generates a one-time token the user can use to link their account.
|
||||
"""
|
||||
check_bot_api_key(x_bot_api_key)
|
||||
|
||||
platform = request.platform.value
|
||||
|
||||
# Check if already linked
|
||||
existing = await PlatformLink.prisma().find_first(
|
||||
where={
|
||||
"platform": platform,
|
||||
"platformUserId": request.platform_user_id,
|
||||
}
|
||||
)
|
||||
if existing:
|
||||
raise HTTPException(
|
||||
status_code=409,
|
||||
detail="This platform account is already linked.",
|
||||
)
|
||||
|
||||
# Invalidate any existing pending tokens for this user
|
||||
await PlatformLinkToken.prisma().update_many(
|
||||
where={
|
||||
"platform": platform,
|
||||
"platformUserId": request.platform_user_id,
|
||||
"usedAt": None,
|
||||
},
|
||||
data={"usedAt": datetime.now(timezone.utc)},
|
||||
)
|
||||
|
||||
# Generate token
|
||||
token = secrets.token_urlsafe(32)
|
||||
expires_at = datetime.now(timezone.utc) + timedelta(
|
||||
minutes=LINK_TOKEN_EXPIRY_MINUTES
|
||||
)
|
||||
|
||||
await PlatformLinkToken.prisma().create(
|
||||
data={
|
||||
"token": token,
|
||||
"platform": platform,
|
||||
"platformUserId": request.platform_user_id,
|
||||
"platformUsername": request.platform_username,
|
||||
"channelId": request.channel_id,
|
||||
"expiresAt": expires_at,
|
||||
}
|
||||
)
|
||||
|
||||
logger.info(
|
||||
"Created link token for %s (expires %s)",
|
||||
platform,
|
||||
expires_at.isoformat(),
|
||||
)
|
||||
|
||||
link_base_url = os.getenv(
|
||||
"PLATFORM_LINK_BASE_URL", "https://platform.agpt.co/link"
|
||||
)
|
||||
link_url = f"{link_base_url}/{token}"
|
||||
|
||||
return LinkTokenResponse(
|
||||
token=token,
|
||||
expires_at=expires_at,
|
||||
link_url=link_url,
|
||||
)
|
||||
|
||||
|
||||
@router.get(
|
||||
"/tokens/{token}/status",
|
||||
response_model=LinkTokenStatusResponse,
|
||||
summary="Check if a link token has been consumed",
|
||||
)
|
||||
async def get_link_token_status(
|
||||
token: TokenPath,
|
||||
x_bot_api_key: str | None = Depends(get_bot_api_key),
|
||||
) -> LinkTokenStatusResponse:
|
||||
"""
|
||||
Called by the bot service to check if a user has completed linking.
|
||||
"""
|
||||
check_bot_api_key(x_bot_api_key)
|
||||
|
||||
link_token = await PlatformLinkToken.prisma().find_unique(where={"token": token})
|
||||
|
||||
if not link_token:
|
||||
raise HTTPException(status_code=404, detail="Token not found")
|
||||
|
||||
if link_token.usedAt is not None:
|
||||
# Token was used — find the linked account
|
||||
link = await PlatformLink.prisma().find_first(
|
||||
where={
|
||||
"platform": link_token.platform,
|
||||
"platformUserId": link_token.platformUserId,
|
||||
}
|
||||
)
|
||||
return LinkTokenStatusResponse(
|
||||
status="linked",
|
||||
user_id=link.userId if link else None,
|
||||
)
|
||||
|
||||
if link_token.expiresAt.replace(tzinfo=timezone.utc) < datetime.now(timezone.utc):
|
||||
return LinkTokenStatusResponse(status="expired")
|
||||
|
||||
return LinkTokenStatusResponse(status="pending")
|
||||
|
||||
|
||||
@router.post(
|
||||
"/resolve",
|
||||
response_model=ResolveResponse,
|
||||
summary="Resolve a platform identity to an AutoGPT user",
|
||||
)
|
||||
async def resolve_platform_user(
|
||||
request: ResolveRequest,
|
||||
x_bot_api_key: str | None = Depends(get_bot_api_key),
|
||||
) -> ResolveResponse:
|
||||
"""
|
||||
Called by the bot service on every incoming message to check if
|
||||
the platform user has a linked AutoGPT account.
|
||||
"""
|
||||
check_bot_api_key(x_bot_api_key)
|
||||
|
||||
link = await PlatformLink.prisma().find_first(
|
||||
where={
|
||||
"platform": request.platform.value,
|
||||
"platformUserId": request.platform_user_id,
|
||||
}
|
||||
)
|
||||
|
||||
if not link:
|
||||
return ResolveResponse(linked=False)
|
||||
|
||||
return ResolveResponse(linked=True, user_id=link.userId)
|
||||
|
||||
|
||||
# ── User-facing endpoints (JWT auth) ──────────────────────────────────
|
||||
|
||||
|
||||
@router.post(
|
||||
"/tokens/{token}/confirm",
|
||||
response_model=ConfirmLinkResponse,
|
||||
dependencies=[Security(auth.requires_user)],
|
||||
summary="Confirm a link token (user must be authenticated)",
|
||||
)
|
||||
async def confirm_link_token(
|
||||
token: TokenPath,
|
||||
user_id: Annotated[str, Security(auth.get_user_id)],
|
||||
) -> ConfirmLinkResponse:
|
||||
"""
|
||||
Called by the frontend when the user clicks the link and is logged in.
|
||||
Consumes the token and creates the platform link.
|
||||
Uses atomic update_many to prevent race conditions on double-click.
|
||||
"""
|
||||
link_token = await PlatformLinkToken.prisma().find_unique(where={"token": token})
|
||||
|
||||
if not link_token:
|
||||
raise HTTPException(status_code=404, detail="Token not found.")
|
||||
|
||||
if link_token.usedAt is not None:
|
||||
raise HTTPException(status_code=410, detail="This link has already been used.")
|
||||
|
||||
if link_token.expiresAt.replace(tzinfo=timezone.utc) < datetime.now(timezone.utc):
|
||||
raise HTTPException(status_code=410, detail="This link has expired.")
|
||||
|
||||
# Atomically mark token as used (only if still unused)
|
||||
updated = await PlatformLinkToken.prisma().update_many(
|
||||
where={"token": token, "usedAt": None},
|
||||
data={"usedAt": datetime.now(timezone.utc)},
|
||||
)
|
||||
|
||||
if updated == 0:
|
||||
raise HTTPException(status_code=410, detail="This link has already been used.")
|
||||
|
||||
# Check if this platform identity is already linked
|
||||
existing = await PlatformLink.prisma().find_first(
|
||||
where={
|
||||
"platform": link_token.platform,
|
||||
"platformUserId": link_token.platformUserId,
|
||||
}
|
||||
)
|
||||
if existing:
|
||||
detail = (
|
||||
"This platform account is already linked to your account."
|
||||
if existing.userId == user_id
|
||||
else "This platform account is already linked to another user."
|
||||
)
|
||||
raise HTTPException(status_code=409, detail=detail)
|
||||
|
||||
# Create the link — catch unique constraint race condition
|
||||
try:
|
||||
await PlatformLink.prisma().create(
|
||||
data={
|
||||
"userId": user_id,
|
||||
"platform": link_token.platform,
|
||||
"platformUserId": link_token.platformUserId,
|
||||
"platformUsername": link_token.platformUsername,
|
||||
}
|
||||
)
|
||||
except Exception as exc:
|
||||
if "unique" in str(exc).lower():
|
||||
raise HTTPException(
|
||||
status_code=409,
|
||||
detail="This platform account was just linked by another request.",
|
||||
) from exc
|
||||
raise
|
||||
|
||||
logger.info(
|
||||
"Linked %s:%s to user ...%s",
|
||||
link_token.platform,
|
||||
link_token.platformUserId,
|
||||
user_id[-8:],
|
||||
)
|
||||
|
||||
return ConfirmLinkResponse(
|
||||
success=True,
|
||||
platform=link_token.platform,
|
||||
platform_user_id=link_token.platformUserId,
|
||||
platform_username=link_token.platformUsername,
|
||||
)
|
||||
|
||||
|
||||
@router.get(
|
||||
"/links",
|
||||
response_model=list[PlatformLinkInfo],
|
||||
dependencies=[Security(auth.requires_user)],
|
||||
summary="List all platform links for the authenticated user",
|
||||
)
|
||||
async def list_my_links(
|
||||
user_id: Annotated[str, Security(auth.get_user_id)],
|
||||
) -> list[PlatformLinkInfo]:
|
||||
"""Returns all platform identities linked to the current user's account."""
|
||||
links = await PlatformLink.prisma().find_many(
|
||||
where={"userId": user_id},
|
||||
order={"linkedAt": "desc"},
|
||||
)
|
||||
|
||||
return [
|
||||
PlatformLinkInfo(
|
||||
id=link.id,
|
||||
platform=link.platform,
|
||||
platform_user_id=link.platformUserId,
|
||||
platform_username=link.platformUsername,
|
||||
linked_at=link.linkedAt,
|
||||
)
|
||||
for link in links
|
||||
]
|
||||
|
||||
|
||||
@router.delete(
|
||||
"/links/{link_id}",
|
||||
response_model=DeleteLinkResponse,
|
||||
dependencies=[Security(auth.requires_user)],
|
||||
summary="Unlink a platform identity",
|
||||
)
|
||||
async def delete_link(
|
||||
link_id: str,
|
||||
user_id: Annotated[str, Security(auth.get_user_id)],
|
||||
) -> DeleteLinkResponse:
|
||||
"""
|
||||
Removes a platform link. The user will need to re-link if they
|
||||
want to use the bot on that platform again.
|
||||
"""
|
||||
link = await PlatformLink.prisma().find_unique(where={"id": link_id})
|
||||
|
||||
if not link:
|
||||
raise HTTPException(status_code=404, detail="Link not found.")
|
||||
|
||||
if link.userId != user_id:
|
||||
raise HTTPException(status_code=403, detail="Not your link.")
|
||||
|
||||
await PlatformLink.prisma().delete(where={"id": link_id})
|
||||
|
||||
logger.info(
|
||||
"Unlinked %s:%s from user ...%s",
|
||||
link.platform,
|
||||
link.platformUserId,
|
||||
user_id[-8:],
|
||||
)
|
||||
|
||||
return DeleteLinkResponse(success=True)
|
||||
@@ -0,0 +1,138 @@
|
||||
"""Tests for platform bot linking API routes."""
|
||||
|
||||
from unittest.mock import patch
|
||||
|
||||
import pytest
|
||||
from fastapi import HTTPException
|
||||
|
||||
from backend.api.features.platform_linking.auth import check_bot_api_key
|
||||
from backend.api.features.platform_linking.models import (
|
||||
ConfirmLinkResponse,
|
||||
CreateLinkTokenRequest,
|
||||
DeleteLinkResponse,
|
||||
LinkTokenStatusResponse,
|
||||
Platform,
|
||||
ResolveRequest,
|
||||
)
|
||||
|
||||
|
||||
class TestPlatformEnum:
|
||||
def test_all_platforms_exist(self):
|
||||
assert Platform.DISCORD.value == "DISCORD"
|
||||
assert Platform.TELEGRAM.value == "TELEGRAM"
|
||||
assert Platform.SLACK.value == "SLACK"
|
||||
assert Platform.TEAMS.value == "TEAMS"
|
||||
assert Platform.WHATSAPP.value == "WHATSAPP"
|
||||
assert Platform.GITHUB.value == "GITHUB"
|
||||
assert Platform.LINEAR.value == "LINEAR"
|
||||
|
||||
|
||||
class TestBotApiKeyAuth:
|
||||
@patch.dict("os.environ", {"PLATFORM_BOT_API_KEY": ""}, clear=False)
|
||||
@patch("backend.api.features.platform_linking.auth.Settings")
|
||||
def test_no_key_configured_allows_when_auth_disabled(self, mock_settings_cls):
|
||||
mock_settings_cls.return_value.config.enable_auth = False
|
||||
check_bot_api_key(None)
|
||||
|
||||
@patch.dict("os.environ", {"PLATFORM_BOT_API_KEY": ""}, clear=False)
|
||||
@patch("backend.api.features.platform_linking.auth.Settings")
|
||||
def test_no_key_configured_rejects_when_auth_enabled(self, mock_settings_cls):
|
||||
mock_settings_cls.return_value.config.enable_auth = True
|
||||
with pytest.raises(HTTPException) as exc_info:
|
||||
check_bot_api_key(None)
|
||||
assert exc_info.value.status_code == 503
|
||||
|
||||
@patch.dict("os.environ", {"PLATFORM_BOT_API_KEY": "secret123"}, clear=False)
|
||||
def test_valid_key(self):
|
||||
check_bot_api_key("secret123")
|
||||
|
||||
@patch.dict("os.environ", {"PLATFORM_BOT_API_KEY": "secret123"}, clear=False)
|
||||
def test_invalid_key_rejected(self):
|
||||
with pytest.raises(HTTPException) as exc_info:
|
||||
check_bot_api_key("wrong")
|
||||
assert exc_info.value.status_code == 401
|
||||
|
||||
@patch.dict("os.environ", {"PLATFORM_BOT_API_KEY": "secret123"}, clear=False)
|
||||
def test_missing_key_rejected(self):
|
||||
with pytest.raises(HTTPException) as exc_info:
|
||||
check_bot_api_key(None)
|
||||
assert exc_info.value.status_code == 401
|
||||
|
||||
|
||||
class TestCreateLinkTokenRequest:
|
||||
def test_valid_request(self):
|
||||
req = CreateLinkTokenRequest(
|
||||
platform=Platform.DISCORD,
|
||||
platform_user_id="353922987235213313",
|
||||
)
|
||||
assert req.platform == Platform.DISCORD
|
||||
assert req.platform_user_id == "353922987235213313"
|
||||
|
||||
def test_empty_platform_user_id_rejected(self):
|
||||
from pydantic import ValidationError
|
||||
|
||||
with pytest.raises(ValidationError):
|
||||
CreateLinkTokenRequest(
|
||||
platform=Platform.DISCORD,
|
||||
platform_user_id="",
|
||||
)
|
||||
|
||||
def test_too_long_platform_user_id_rejected(self):
|
||||
from pydantic import ValidationError
|
||||
|
||||
with pytest.raises(ValidationError):
|
||||
CreateLinkTokenRequest(
|
||||
platform=Platform.DISCORD,
|
||||
platform_user_id="x" * 256,
|
||||
)
|
||||
|
||||
def test_invalid_platform_rejected(self):
|
||||
from pydantic import ValidationError
|
||||
|
||||
with pytest.raises(ValidationError):
|
||||
CreateLinkTokenRequest.model_validate(
|
||||
{"platform": "INVALID", "platform_user_id": "123"}
|
||||
)
|
||||
|
||||
|
||||
class TestResolveRequest:
|
||||
def test_valid_request(self):
|
||||
req = ResolveRequest(
|
||||
platform=Platform.TELEGRAM,
|
||||
platform_user_id="123456789",
|
||||
)
|
||||
assert req.platform == Platform.TELEGRAM
|
||||
|
||||
def test_empty_id_rejected(self):
|
||||
from pydantic import ValidationError
|
||||
|
||||
with pytest.raises(ValidationError):
|
||||
ResolveRequest(
|
||||
platform=Platform.SLACK,
|
||||
platform_user_id="",
|
||||
)
|
||||
|
||||
|
||||
class TestResponseModels:
|
||||
def test_link_token_status_literal(self):
|
||||
resp = LinkTokenStatusResponse(status="pending")
|
||||
assert resp.status == "pending"
|
||||
|
||||
resp = LinkTokenStatusResponse(status="linked", user_id="abc")
|
||||
assert resp.status == "linked"
|
||||
|
||||
resp = LinkTokenStatusResponse(status="expired")
|
||||
assert resp.status == "expired"
|
||||
|
||||
def test_confirm_link_response(self):
|
||||
resp = ConfirmLinkResponse(
|
||||
success=True,
|
||||
platform="DISCORD",
|
||||
platform_user_id="123",
|
||||
platform_username="testuser",
|
||||
)
|
||||
assert resp.success is True
|
||||
|
||||
def test_delete_link_response(self):
|
||||
resp = DeleteLinkResponse(success=True)
|
||||
assert resp.success is True
|
||||
@@ -30,6 +30,8 @@ import backend.api.features.library.routes
|
||||
import backend.api.features.mcp.routes as mcp_routes
|
||||
import backend.api.features.oauth
|
||||
import backend.api.features.otto.routes
|
||||
import backend.api.features.platform_linking.chat_proxy
|
||||
import backend.api.features.platform_linking.routes
|
||||
import backend.api.features.postmark.postmark
|
||||
import backend.api.features.store.model
|
||||
import backend.api.features.store.routes
|
||||
@@ -361,6 +363,16 @@ app.include_router(
|
||||
tags=["oauth"],
|
||||
prefix="/api/oauth",
|
||||
)
|
||||
app.include_router(
|
||||
backend.api.features.platform_linking.routes.router,
|
||||
tags=["platform-linking"],
|
||||
prefix="/api/platform-linking",
|
||||
)
|
||||
app.include_router(
|
||||
backend.api.features.platform_linking.chat_proxy.router,
|
||||
tags=["platform-linking"],
|
||||
prefix="/api/platform-linking",
|
||||
)
|
||||
|
||||
app.mount("/external-api", external_api)
|
||||
|
||||
|
||||
@@ -0,0 +1,44 @@
|
||||
-- CreateEnum
|
||||
CREATE TYPE "PlatformType" AS ENUM ('DISCORD', 'TELEGRAM', 'SLACK', 'TEAMS', 'WHATSAPP', 'GITHUB', 'LINEAR');
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "PlatformLink" (
|
||||
"id" TEXT NOT NULL,
|
||||
"userId" TEXT NOT NULL,
|
||||
"platform" "PlatformType" NOT NULL,
|
||||
"platformUserId" TEXT NOT NULL,
|
||||
"platformUsername" TEXT,
|
||||
"linkedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
|
||||
CONSTRAINT "PlatformLink_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "PlatformLinkToken" (
|
||||
"id" TEXT NOT NULL,
|
||||
"token" TEXT NOT NULL,
|
||||
"platform" "PlatformType" NOT NULL,
|
||||
"platformUserId" TEXT NOT NULL,
|
||||
"platformUsername" TEXT,
|
||||
"channelId" TEXT,
|
||||
"expiresAt" TIMESTAMP(3) NOT NULL,
|
||||
"usedAt" TIMESTAMP(3),
|
||||
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
|
||||
CONSTRAINT "PlatformLinkToken_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "PlatformLink_platform_platformUserId_key" ON "PlatformLink"("platform", "platformUserId");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "PlatformLink_userId_idx" ON "PlatformLink"("userId");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "PlatformLinkToken_token_key" ON "PlatformLinkToken"("token");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "PlatformLinkToken_expiresAt_idx" ON "PlatformLinkToken"("expiresAt");
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "PlatformLink" ADD CONSTRAINT "PlatformLink_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
@@ -71,6 +71,9 @@ model User {
|
||||
OAuthAuthorizationCodes OAuthAuthorizationCode[]
|
||||
OAuthAccessTokens OAuthAccessToken[]
|
||||
OAuthRefreshTokens OAuthRefreshToken[]
|
||||
|
||||
// Platform bot linking
|
||||
PlatformLinks PlatformLink[]
|
||||
}
|
||||
|
||||
enum OnboardingStep {
|
||||
@@ -1302,3 +1305,50 @@ model OAuthRefreshToken {
|
||||
@@index([userId, applicationId])
|
||||
@@index([expiresAt]) // For cleanup
|
||||
}
|
||||
|
||||
// ── Platform Bot Linking ──────────────────────────────────────────────
|
||||
// Links external chat platform identities (Discord, Telegram, Slack, etc.)
|
||||
// to AutoGPT user accounts, enabling the multi-platform CoPilot bot.
|
||||
|
||||
enum PlatformType {
|
||||
DISCORD
|
||||
TELEGRAM
|
||||
SLACK
|
||||
TEAMS
|
||||
WHATSAPP
|
||||
GITHUB
|
||||
LINEAR
|
||||
}
|
||||
|
||||
// Maps a platform user identity to an AutoGPT account.
|
||||
// One AutoGPT user can have multiple platform links (e.g. Discord + Telegram).
|
||||
// Each platform identity can only link to one AutoGPT account.
|
||||
model PlatformLink {
|
||||
id String @id @default(uuid())
|
||||
userId String
|
||||
User User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
||||
platform PlatformType
|
||||
platformUserId String // The user's ID on that platform
|
||||
platformUsername String? // Display name (best-effort, may go stale)
|
||||
linkedAt DateTime @default(now())
|
||||
|
||||
@@unique([platform, platformUserId])
|
||||
@@index([userId])
|
||||
}
|
||||
|
||||
// One-time tokens for the account linking flow.
|
||||
// Generated when an unlinked user messages the bot; consumed when they
|
||||
// complete the link on the AutoGPT web app.
|
||||
model PlatformLinkToken {
|
||||
id String @id @default(uuid())
|
||||
token String @unique
|
||||
platform PlatformType
|
||||
platformUserId String
|
||||
platformUsername String?
|
||||
channelId String? // So the bot can send a confirmation message
|
||||
expiresAt DateTime
|
||||
usedAt DateTime?
|
||||
createdAt DateTime @default(now())
|
||||
|
||||
@@index([expiresAt])
|
||||
}
|
||||
|
||||
@@ -5457,6 +5457,290 @@
|
||||
"security": [{ "HTTPBearerJWT": [] }]
|
||||
}
|
||||
},
|
||||
"/api/platform-linking/chat/session": {
|
||||
"post": {
|
||||
"tags": ["platform-linking"],
|
||||
"summary": "Create a CoPilot session for a linked user (bot-facing)",
|
||||
"description": "Creates a new CoPilot chat session on behalf of a linked user.",
|
||||
"operationId": "postPlatform-linkingCreate a copilot session for a linked user (bot-facing)",
|
||||
"requestBody": {
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": { "$ref": "#/components/schemas/BotChatRequest" }
|
||||
}
|
||||
},
|
||||
"required": true
|
||||
},
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "Successful Response",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/BotChatSessionResponse"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"422": {
|
||||
"description": "Validation Error",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": { "$ref": "#/components/schemas/HTTPValidationError" }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/api/platform-linking/chat/stream": {
|
||||
"post": {
|
||||
"tags": ["platform-linking"],
|
||||
"summary": "Stream a CoPilot response for a linked user (bot-facing)",
|
||||
"description": "Send a message to CoPilot on behalf of a linked user and stream\nthe response back as Server-Sent Events.\n\nThe bot authenticates with its API key — no user JWT needed.",
|
||||
"operationId": "postPlatform-linkingStream a copilot response for a linked user (bot-facing)",
|
||||
"requestBody": {
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": { "$ref": "#/components/schemas/BotChatRequest" }
|
||||
}
|
||||
},
|
||||
"required": true
|
||||
},
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "Successful Response",
|
||||
"content": { "application/json": { "schema": {} } }
|
||||
},
|
||||
"422": {
|
||||
"description": "Validation Error",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": { "$ref": "#/components/schemas/HTTPValidationError" }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/api/platform-linking/links": {
|
||||
"get": {
|
||||
"tags": ["platform-linking"],
|
||||
"summary": "List all platform links for the authenticated user",
|
||||
"description": "Returns all platform identities linked to the current user's account.",
|
||||
"operationId": "getPlatform-linkingList all platform links for the authenticated user",
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "Successful Response",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"items": { "$ref": "#/components/schemas/PlatformLinkInfo" },
|
||||
"type": "array",
|
||||
"title": "Response Getplatform-Linkinglist All Platform Links For The Authenticated User"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"401": {
|
||||
"$ref": "#/components/responses/HTTP401NotAuthenticatedError"
|
||||
}
|
||||
},
|
||||
"security": [{ "HTTPBearerJWT": [] }]
|
||||
}
|
||||
},
|
||||
"/api/platform-linking/links/{link_id}": {
|
||||
"delete": {
|
||||
"tags": ["platform-linking"],
|
||||
"summary": "Unlink a platform identity",
|
||||
"description": "Removes a platform link. The user will need to re-link if they\nwant to use the bot on that platform again.",
|
||||
"operationId": "deletePlatform-linkingUnlink a platform identity",
|
||||
"security": [{ "HTTPBearerJWT": [] }],
|
||||
"parameters": [
|
||||
{
|
||||
"name": "link_id",
|
||||
"in": "path",
|
||||
"required": true,
|
||||
"schema": { "type": "string", "title": "Link Id" }
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "Successful Response",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": { "$ref": "#/components/schemas/DeleteLinkResponse" }
|
||||
}
|
||||
}
|
||||
},
|
||||
"401": {
|
||||
"$ref": "#/components/responses/HTTP401NotAuthenticatedError"
|
||||
},
|
||||
"422": {
|
||||
"description": "Validation Error",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": { "$ref": "#/components/schemas/HTTPValidationError" }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/api/platform-linking/resolve": {
|
||||
"post": {
|
||||
"tags": ["platform-linking"],
|
||||
"summary": "Resolve a platform identity to an AutoGPT user",
|
||||
"description": "Called by the bot service on every incoming message to check if\nthe platform user has a linked AutoGPT account.",
|
||||
"operationId": "postPlatform-linkingResolve a platform identity to an autogpt user",
|
||||
"requestBody": {
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": { "$ref": "#/components/schemas/ResolveRequest" }
|
||||
}
|
||||
},
|
||||
"required": true
|
||||
},
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "Successful Response",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": { "$ref": "#/components/schemas/ResolveResponse" }
|
||||
}
|
||||
}
|
||||
},
|
||||
"422": {
|
||||
"description": "Validation Error",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": { "$ref": "#/components/schemas/HTTPValidationError" }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/api/platform-linking/tokens": {
|
||||
"post": {
|
||||
"tags": ["platform-linking"],
|
||||
"summary": "Create a link token for an unlinked platform user",
|
||||
"description": "Called by the bot service when it encounters an unlinked user.\nGenerates a one-time token the user can use to link their account.",
|
||||
"operationId": "postPlatform-linkingCreate a link token for an unlinked platform user",
|
||||
"requestBody": {
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/CreateLinkTokenRequest"
|
||||
}
|
||||
}
|
||||
},
|
||||
"required": true
|
||||
},
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "Successful Response",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": { "$ref": "#/components/schemas/LinkTokenResponse" }
|
||||
}
|
||||
}
|
||||
},
|
||||
"422": {
|
||||
"description": "Validation Error",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": { "$ref": "#/components/schemas/HTTPValidationError" }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/api/platform-linking/tokens/{token}/confirm": {
|
||||
"post": {
|
||||
"tags": ["platform-linking"],
|
||||
"summary": "Confirm a link token (user must be authenticated)",
|
||||
"description": "Called by the frontend when the user clicks the link and is logged in.\nConsumes the token and creates the platform link.\nUses atomic update_many to prevent race conditions on double-click.",
|
||||
"operationId": "postPlatform-linkingConfirm a link token (user must be authenticated)",
|
||||
"security": [{ "HTTPBearerJWT": [] }],
|
||||
"parameters": [
|
||||
{
|
||||
"name": "token",
|
||||
"in": "path",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"type": "string",
|
||||
"maxLength": 64,
|
||||
"pattern": "^[A-Za-z0-9_-]+$",
|
||||
"title": "Token"
|
||||
}
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "Successful Response",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": { "$ref": "#/components/schemas/ConfirmLinkResponse" }
|
||||
}
|
||||
}
|
||||
},
|
||||
"401": {
|
||||
"$ref": "#/components/responses/HTTP401NotAuthenticatedError"
|
||||
},
|
||||
"422": {
|
||||
"description": "Validation Error",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": { "$ref": "#/components/schemas/HTTPValidationError" }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/api/platform-linking/tokens/{token}/status": {
|
||||
"get": {
|
||||
"tags": ["platform-linking"],
|
||||
"summary": "Check if a link token has been consumed",
|
||||
"description": "Called by the bot service to check if a user has completed linking.",
|
||||
"operationId": "getPlatform-linkingCheck if a link token has been consumed",
|
||||
"parameters": [
|
||||
{
|
||||
"name": "token",
|
||||
"in": "path",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"type": "string",
|
||||
"maxLength": 64,
|
||||
"pattern": "^[A-Za-z0-9_-]+$",
|
||||
"title": "Token"
|
||||
}
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "Successful Response",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/LinkTokenStatusResponse"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"422": {
|
||||
"description": "Validation Error",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": { "$ref": "#/components/schemas/HTTPValidationError" }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/api/public/shared/{share_token}": {
|
||||
"get": {
|
||||
"tags": ["v1"],
|
||||
@@ -8312,6 +8596,40 @@
|
||||
"required": ["file"],
|
||||
"title": "Body_postWorkspaceUpload file to workspace"
|
||||
},
|
||||
"BotChatRequest": {
|
||||
"properties": {
|
||||
"user_id": {
|
||||
"type": "string",
|
||||
"title": "User Id",
|
||||
"description": "The linked AutoGPT user ID"
|
||||
},
|
||||
"message": {
|
||||
"type": "string",
|
||||
"maxLength": 32000,
|
||||
"minLength": 1,
|
||||
"title": "Message",
|
||||
"description": "The user's message"
|
||||
},
|
||||
"session_id": {
|
||||
"anyOf": [{ "type": "string" }, { "type": "null" }],
|
||||
"title": "Session Id",
|
||||
"description": "Existing chat session ID. If omitted, a new session is created."
|
||||
}
|
||||
},
|
||||
"type": "object",
|
||||
"required": ["user_id", "message"],
|
||||
"title": "BotChatRequest",
|
||||
"description": "Request from the bot to chat as a linked user."
|
||||
},
|
||||
"BotChatSessionResponse": {
|
||||
"properties": {
|
||||
"session_id": { "type": "string", "title": "Session Id" }
|
||||
},
|
||||
"type": "object",
|
||||
"required": ["session_id"],
|
||||
"title": "BotChatSessionResponse",
|
||||
"description": "Returned when creating a new session via the bot proxy."
|
||||
},
|
||||
"BulkMoveAgentsRequest": {
|
||||
"properties": {
|
||||
"agent_ids": {
|
||||
@@ -8427,6 +8745,25 @@
|
||||
"title": "CoPilotUsageStatus",
|
||||
"description": "Current usage status for a user across all windows."
|
||||
},
|
||||
"ConfirmLinkResponse": {
|
||||
"properties": {
|
||||
"success": { "type": "boolean", "title": "Success" },
|
||||
"platform": { "type": "string", "title": "Platform" },
|
||||
"platform_user_id": { "type": "string", "title": "Platform User Id" },
|
||||
"platform_username": {
|
||||
"anyOf": [{ "type": "string" }, { "type": "null" }],
|
||||
"title": "Platform Username"
|
||||
}
|
||||
},
|
||||
"type": "object",
|
||||
"required": [
|
||||
"success",
|
||||
"platform",
|
||||
"platform_user_id",
|
||||
"platform_username"
|
||||
],
|
||||
"title": "ConfirmLinkResponse"
|
||||
},
|
||||
"ContentType": {
|
||||
"type": "string",
|
||||
"enum": [
|
||||
@@ -8504,6 +8841,41 @@
|
||||
"required": ["graph"],
|
||||
"title": "CreateGraph"
|
||||
},
|
||||
"CreateLinkTokenRequest": {
|
||||
"properties": {
|
||||
"platform": {
|
||||
"$ref": "#/components/schemas/Platform",
|
||||
"description": "Platform name"
|
||||
},
|
||||
"platform_user_id": {
|
||||
"type": "string",
|
||||
"maxLength": 255,
|
||||
"minLength": 1,
|
||||
"title": "Platform User Id",
|
||||
"description": "The user's ID on the platform"
|
||||
},
|
||||
"platform_username": {
|
||||
"anyOf": [
|
||||
{ "type": "string", "maxLength": 255 },
|
||||
{ "type": "null" }
|
||||
],
|
||||
"title": "Platform Username",
|
||||
"description": "Display name (best effort)"
|
||||
},
|
||||
"channel_id": {
|
||||
"anyOf": [
|
||||
{ "type": "string", "maxLength": 255 },
|
||||
{ "type": "null" }
|
||||
],
|
||||
"title": "Channel Id",
|
||||
"description": "Channel ID for sending confirmation back"
|
||||
}
|
||||
},
|
||||
"type": "object",
|
||||
"required": ["platform", "platform_user_id"],
|
||||
"title": "CreateLinkTokenRequest",
|
||||
"description": "Request from the bot service to create a linking token."
|
||||
},
|
||||
"CreateSessionResponse": {
|
||||
"properties": {
|
||||
"id": { "type": "string", "title": "Id" },
|
||||
@@ -8686,6 +9058,12 @@
|
||||
"required": ["version_counts"],
|
||||
"title": "DeleteGraphResponse"
|
||||
},
|
||||
"DeleteLinkResponse": {
|
||||
"properties": { "success": { "type": "boolean", "title": "Success" } },
|
||||
"type": "object",
|
||||
"required": ["success"],
|
||||
"title": "DeleteLinkResponse"
|
||||
},
|
||||
"DiscoverToolsRequest": {
|
||||
"properties": {
|
||||
"server_url": {
|
||||
@@ -10457,6 +10835,36 @@
|
||||
"required": ["source_id", "sink_id", "source_name", "sink_name"],
|
||||
"title": "Link"
|
||||
},
|
||||
"LinkTokenResponse": {
|
||||
"properties": {
|
||||
"token": { "type": "string", "title": "Token" },
|
||||
"expires_at": {
|
||||
"type": "string",
|
||||
"format": "date-time",
|
||||
"title": "Expires At"
|
||||
},
|
||||
"link_url": { "type": "string", "title": "Link Url" }
|
||||
},
|
||||
"type": "object",
|
||||
"required": ["token", "expires_at", "link_url"],
|
||||
"title": "LinkTokenResponse"
|
||||
},
|
||||
"LinkTokenStatusResponse": {
|
||||
"properties": {
|
||||
"status": {
|
||||
"type": "string",
|
||||
"enum": ["pending", "linked", "expired"],
|
||||
"title": "Status"
|
||||
},
|
||||
"user_id": {
|
||||
"anyOf": [{ "type": "string" }, { "type": "null" }],
|
||||
"title": "User Id"
|
||||
}
|
||||
},
|
||||
"type": "object",
|
||||
"required": ["status"],
|
||||
"title": "LinkTokenStatusResponse"
|
||||
},
|
||||
"ListSessionsResponse": {
|
||||
"properties": {
|
||||
"sessions": {
|
||||
@@ -11296,6 +11704,45 @@
|
||||
"title": "PendingHumanReviewModel",
|
||||
"description": "Response model for pending human review data.\n\nRepresents a human review request that is awaiting user action.\nContains all necessary information for a user to review and approve\nor reject data from a Human-in-the-Loop block execution.\n\nAttributes:\n id: Unique identifier for the review record\n user_id: ID of the user who must perform the review\n node_exec_id: ID of the node execution that created this review\n node_id: ID of the node definition (for grouping reviews from same node)\n graph_exec_id: ID of the graph execution containing the node\n graph_id: ID of the graph template being executed\n graph_version: Version number of the graph template\n payload: The actual data payload awaiting review\n instructions: Instructions or message for the reviewer\n editable: Whether the reviewer can edit the data\n status: Current review status (WAITING, APPROVED, or REJECTED)\n review_message: Optional message from the reviewer\n created_at: Timestamp when review was created\n updated_at: Timestamp when review was last modified\n reviewed_at: Timestamp when review was completed (if applicable)"
|
||||
},
|
||||
"Platform": {
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"DISCORD",
|
||||
"TELEGRAM",
|
||||
"SLACK",
|
||||
"TEAMS",
|
||||
"WHATSAPP",
|
||||
"GITHUB",
|
||||
"LINEAR"
|
||||
],
|
||||
"title": "Platform",
|
||||
"description": "Supported platform types (mirrors Prisma PlatformType)."
|
||||
},
|
||||
"PlatformLinkInfo": {
|
||||
"properties": {
|
||||
"id": { "type": "string", "title": "Id" },
|
||||
"platform": { "type": "string", "title": "Platform" },
|
||||
"platform_user_id": { "type": "string", "title": "Platform User Id" },
|
||||
"platform_username": {
|
||||
"anyOf": [{ "type": "string" }, { "type": "null" }],
|
||||
"title": "Platform Username"
|
||||
},
|
||||
"linked_at": {
|
||||
"type": "string",
|
||||
"format": "date-time",
|
||||
"title": "Linked At"
|
||||
}
|
||||
},
|
||||
"type": "object",
|
||||
"required": [
|
||||
"id",
|
||||
"platform",
|
||||
"platform_user_id",
|
||||
"platform_username",
|
||||
"linked_at"
|
||||
],
|
||||
"title": "PlatformLinkInfo"
|
||||
},
|
||||
"PostmarkBounceEnum": {
|
||||
"type": "integer",
|
||||
"enum": [
|
||||
@@ -11818,6 +12265,33 @@
|
||||
"required": ["credit_amount"],
|
||||
"title": "RequestTopUp"
|
||||
},
|
||||
"ResolveRequest": {
|
||||
"properties": {
|
||||
"platform": { "$ref": "#/components/schemas/Platform" },
|
||||
"platform_user_id": {
|
||||
"type": "string",
|
||||
"maxLength": 255,
|
||||
"minLength": 1,
|
||||
"title": "Platform User Id"
|
||||
}
|
||||
},
|
||||
"type": "object",
|
||||
"required": ["platform", "platform_user_id"],
|
||||
"title": "ResolveRequest",
|
||||
"description": "Resolve a platform identity to an AutoGPT user."
|
||||
},
|
||||
"ResolveResponse": {
|
||||
"properties": {
|
||||
"linked": { "type": "boolean", "title": "Linked" },
|
||||
"user_id": {
|
||||
"anyOf": [{ "type": "string" }, { "type": "null" }],
|
||||
"title": "User Id"
|
||||
}
|
||||
},
|
||||
"type": "object",
|
||||
"required": ["linked"],
|
||||
"title": "ResolveResponse"
|
||||
},
|
||||
"ResponseType": {
|
||||
"type": "string",
|
||||
"enum": [
|
||||
|
||||
Reference in New Issue
Block a user