Skip to content

Commit ebb4ca0

Browse files
eureka0928claudeWendong-Fan
authored
feat: Add LinkedIn OAuth integration with CAMEL-AI toolkit (#1104)
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com> Co-authored-by: Wendong-Fan <w3ndong.fan@gmail.com> Co-authored-by: Wendong-Fan <133094783+Wendong-Fan@users.noreply.github.com>
1 parent 4b8394d commit ebb4ca0

6 files changed

Lines changed: 622 additions & 30 deletions

File tree

backend/app/controller/tool_controller.py

Lines changed: 228 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,18 +12,35 @@
1212
# limitations under the License.
1313
# ========= Copyright 2025-2026 @ Eigent.ai All Rights Reserved. =========
1414

15+
from typing import Optional
1516
from fastapi import APIRouter, HTTPException
17+
from pydantic import BaseModel
1618
from app.utils.toolkit.notion_mcp_toolkit import NotionMCPToolkit
1719
from app.utils.toolkit.google_calendar_toolkit import GoogleCalendarToolkit
20+
from app.utils.toolkit.linkedin_toolkit import LinkedInToolkit
1821
from app.utils.oauth_state_manager import oauth_state_manager
1922
import logging
23+
24+
2025
from camel.toolkits.hybrid_browser_toolkit.hybrid_browser_toolkit_ts import (
2126
HybridBrowserToolkit as BaseHybridBrowserToolkit,
2227
)
2328
from app.utils.cookie_manager import CookieManager
29+
30+
2431
import os
32+
import time
2533
import uuid
2634

35+
36+
class LinkedInTokenRequest(BaseModel):
37+
r"""Request model for saving LinkedIn OAuth token."""
38+
access_token: str
39+
refresh_token: Optional[str] = None
40+
expires_in: Optional[int] = None
41+
scope: Optional[str] = None
42+
43+
2744
logger = logging.getLogger("tool_controller")
2845
router = APIRouter()
2946

@@ -118,10 +135,81 @@ async def install_tool(tool: str):
118135
status_code=500,
119136
detail=f"Failed to install {tool}: {str(e)}"
120137
)
138+
elif tool == "linkedin":
139+
try:
140+
# Check if LinkedIn is already authenticated
141+
if LinkedInToolkit.is_authenticated():
142+
# Check if token is expired
143+
if LinkedInToolkit.is_token_expired():
144+
logger.info("LinkedIn token has expired")
145+
return {
146+
"success": False,
147+
"status": "token_expired",
148+
"message": "LinkedIn token has expired. Please re-authenticate via OAuth.",
149+
"toolkit_name": "LinkedInToolkit",
150+
"requires_auth": True,
151+
"oauth_url": "/api/oauth/linkedin/login"
152+
}
153+
154+
try:
155+
toolkit = LinkedInToolkit("install_auth")
156+
tools = [tool_func.func.__name__ for tool_func in toolkit.get_tools()]
157+
158+
# Try to get profile to verify token is valid
159+
profile = toolkit.get_profile_safe()
160+
161+
# Check if token is expiring soon
162+
token_warning = None
163+
if LinkedInToolkit.is_token_expiring_soon():
164+
token_info = LinkedInToolkit.get_token_info()
165+
if token_info and token_info.get("expires_at"):
166+
days_remaining = (token_info["expires_at"] - int(time.time())) // (24 * 60 * 60)
167+
token_warning = f"Token expires in {days_remaining} days. Consider re-authenticating soon."
168+
169+
logger.info(f"Successfully initialized LinkedIn toolkit with {len(tools)} tools")
170+
result = {
171+
"success": True,
172+
"tools": tools,
173+
"message": f"Successfully installed {tool} toolkit",
174+
"count": len(tools),
175+
"toolkit_name": "LinkedInToolkit",
176+
"profile": profile if "error" not in profile else None
177+
}
178+
if token_warning:
179+
result["warning"] = token_warning
180+
return result
181+
except Exception as e:
182+
logger.warning(f"LinkedIn token may be invalid: {e}")
183+
# Token exists but may be expired/invalid
184+
return {
185+
"success": False,
186+
"status": "token_invalid",
187+
"message": "LinkedIn token may be expired or invalid. Please re-authenticate via OAuth.",
188+
"toolkit_name": "LinkedInToolkit",
189+
"requires_auth": True,
190+
"oauth_url": "/api/oauth/linkedin/login"
191+
}
192+
else:
193+
# No credentials - need OAuth authorization
194+
logger.info("No LinkedIn credentials found, OAuth required")
195+
return {
196+
"success": False,
197+
"status": "not_configured",
198+
"message": "LinkedIn OAuth required. Redirect user to OAuth login.",
199+
"toolkit_name": "LinkedInToolkit",
200+
"requires_auth": True,
201+
"oauth_url": "/api/oauth/linkedin/login"
202+
}
203+
except Exception as e:
204+
logger.error(f"Failed to install {tool} toolkit: {e}")
205+
raise HTTPException(
206+
status_code=500,
207+
detail=f"Failed to install {tool}: {str(e)}"
208+
)
121209
else:
122210
raise HTTPException(
123211
status_code=404,
124-
detail=f"Tool '{tool}' not found. Available tools: ['notion', 'google_calendar']"
212+
detail=f"Tool '{tool}' not found. Available tools: ['notion', 'google_calendar', 'linkedin']"
125213
)
126214

127215

@@ -148,6 +236,14 @@ async def list_available_tools():
148236
"description": "Google Calendar integration for managing events and schedules",
149237
"toolkit_class": "GoogleCalendarToolkit",
150238
"requires_auth": True
239+
},
240+
{
241+
"name": "linkedin",
242+
"display_name": "LinkedIn",
243+
"description": "LinkedIn integration for creating posts, managing profile, and social media automation",
244+
"toolkit_class": "LinkedInToolkit",
245+
"requires_auth": True,
246+
"oauth_url": "/api/oauth/linkedin/login"
151247
}
152248
]
153249
}
@@ -309,10 +405,140 @@ async def uninstall_tool(tool: str):
309405
status_code=500,
310406
detail=f"Failed to uninstall {tool}: {str(e)}"
311407
)
408+
elif tool == "linkedin":
409+
try:
410+
# Clear LinkedIn token
411+
success = LinkedInToolkit.clear_token()
412+
413+
if success:
414+
return {
415+
"success": True,
416+
"message": f"Successfully uninstalled {tool} and cleaned up authentication tokens"
417+
}
418+
else:
419+
return {
420+
"success": True,
421+
"message": f"Uninstalled {tool} (no tokens found to clean up)"
422+
}
423+
except Exception as e:
424+
logger.error(f"Failed to uninstall {tool}: {e}")
425+
raise HTTPException(
426+
status_code=500,
427+
detail=f"Failed to uninstall {tool}: {str(e)}"
428+
)
312429
else:
313430
raise HTTPException(
314431
status_code=404,
315-
detail=f"Tool '{tool}' not found. Available tools: ['notion', 'google_calendar']"
432+
detail=f"Tool '{tool}' not found. Available tools: ['notion', 'google_calendar', 'linkedin']"
433+
)
434+
435+
436+
@router.post("/linkedin/save-token", name="save linkedin token")
437+
async def save_linkedin_token(token_request: LinkedInTokenRequest):
438+
r"""Save LinkedIn OAuth token after successful authorization.
439+
440+
Args:
441+
token_request: Token data containing access_token and optionally refresh_token
442+
443+
Returns:
444+
Save result with tool information
445+
"""
446+
try:
447+
token_data = token_request.model_dump(exclude_none=True)
448+
449+
# Save the token
450+
success = LinkedInToolkit.save_token(token_data)
451+
452+
if success:
453+
# Verify the token works by initializing toolkit
454+
try:
455+
toolkit = LinkedInToolkit("install_auth")
456+
tools = [tool_func.func.__name__ for tool_func in toolkit.get_tools()]
457+
profile = toolkit.get_profile_safe()
458+
459+
return {
460+
"success": True,
461+
"message": "LinkedIn token saved successfully",
462+
"tools": tools,
463+
"count": len(tools),
464+
"profile": profile if "error" not in profile else None
465+
}
466+
except Exception as e:
467+
logger.warning(f"Token saved but verification failed: {e}")
468+
return {
469+
"success": True,
470+
"message": "LinkedIn token saved (verification pending)",
471+
"warning": str(e)
472+
}
473+
else:
474+
raise HTTPException(
475+
status_code=500,
476+
detail="Failed to save LinkedIn token"
477+
)
478+
except HTTPException:
479+
raise
480+
except Exception as e:
481+
logger.error(f"Failed to save LinkedIn token: {e}")
482+
raise HTTPException(
483+
status_code=500,
484+
detail=f"Failed to save token: {str(e)}"
485+
)
486+
487+
488+
@router.get("/linkedin/status", name="get linkedin status")
489+
async def get_linkedin_status():
490+
r"""Get current LinkedIn authentication status and token info.
491+
492+
Returns:
493+
Status information including authentication state and token expiry
494+
"""
495+
try:
496+
is_authenticated = LinkedInToolkit.is_authenticated()
497+
498+
if not is_authenticated:
499+
return {
500+
"authenticated": False,
501+
"status": "not_configured",
502+
"message": "LinkedIn not configured. OAuth required.",
503+
"oauth_url": "/api/oauth/linkedin/login"
504+
}
505+
506+
token_info = LinkedInToolkit.get_token_info()
507+
is_expired = LinkedInToolkit.is_token_expired()
508+
is_expiring_soon = LinkedInToolkit.is_token_expiring_soon()
509+
510+
result = {
511+
"authenticated": True,
512+
"status": "expired" if is_expired else ("expiring_soon" if is_expiring_soon else "valid"),
513+
}
514+
515+
if token_info:
516+
if token_info.get("expires_at"):
517+
current_time = int(time.time())
518+
expires_at = token_info["expires_at"]
519+
seconds_remaining = max(0, expires_at - current_time)
520+
days_remaining = seconds_remaining // (24 * 60 * 60)
521+
result["expires_at"] = expires_at
522+
result["days_remaining"] = days_remaining
523+
524+
if token_info.get("saved_at"):
525+
result["saved_at"] = token_info["saved_at"]
526+
527+
if is_expired:
528+
result["message"] = "Token has expired. Please re-authenticate."
529+
result["oauth_url"] = "/api/oauth/linkedin/login"
530+
elif is_expiring_soon:
531+
result["message"] = f"Token expires in {result.get('days_remaining', 'unknown')} days. Consider re-authenticating."
532+
result["oauth_url"] = "/api/oauth/linkedin/login"
533+
else:
534+
result["message"] = "LinkedIn is connected and token is valid."
535+
536+
return result
537+
except Exception as e:
538+
logger.error(f"Failed to get LinkedIn status: {e}")
539+
raise HTTPException(
540+
status_code=500,
541+
detail=f"Failed to get status: {str(e)}"
316542
)
317543

318544

0 commit comments

Comments
 (0)