1212# limitations under the License.
1313# ========= Copyright 2025-2026 @ Eigent.ai All Rights Reserved. =========
1414
15+ from typing import Optional
1516from fastapi import APIRouter , HTTPException
17+ from pydantic import BaseModel
1618from app .utils .toolkit .notion_mcp_toolkit import NotionMCPToolkit
1719from app .utils .toolkit .google_calendar_toolkit import GoogleCalendarToolkit
20+ from app .utils .toolkit .linkedin_toolkit import LinkedInToolkit
1821from app .utils .oauth_state_manager import oauth_state_manager
1922import logging
23+
24+
2025from camel .toolkits .hybrid_browser_toolkit .hybrid_browser_toolkit_ts import (
2126 HybridBrowserToolkit as BaseHybridBrowserToolkit ,
2227)
2328from app .utils .cookie_manager import CookieManager
29+
30+
2431import os
32+ import time
2533import 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+
2744logger = logging .getLogger ("tool_controller" )
2845router = 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