arcade-mcp/toolkits/linkedin/arcade_linkedin/tools/share.py
Nate Barbettini cc3aba61cf
LinkedIn.CreateTextPost: Fix tool execution error (#94)
Silly typo caused a bug.
2024-10-07 09:08:30 -07:00

114 lines
3.9 KiB
Python

from typing import Annotated
import httpx
from arcade.core.errors import ToolExecutionError
from arcade.core.schema import ToolContext
from arcade.sdk import tool
from arcade.sdk.auth import LinkedIn
LINKEDIN_BASE_URL = "https://api.linkedin.com/v2"
async def _send_linkedin_request(
context: ToolContext,
method: str,
endpoint: str,
params: dict | None = None,
json_data: dict | None = None,
) -> httpx.Response:
"""
Send an asynchronous request to the LinkedIn API.
Args:
context: The tool context containing the authorization token.
method: The HTTP method (GET, POST, PUT, DELETE, etc.).
endpoint: The API endpoint path (e.g., "/ugcPosts").
params: Query parameters to include in the request.
json_data: JSON data to include in the request body.
Returns:
The response object from the API request.
Raises:
ToolExecutionError: If the request fails for any reason.
"""
url = f"{LINKEDIN_BASE_URL}{endpoint}"
headers = {"Authorization": f"Bearer {context.authorization.token}"}
async with httpx.AsyncClient() as client:
try:
response = await client.request(
method, url, headers=headers, params=params, json=json_data
)
response.raise_for_status()
except httpx.RequestError as e:
raise ToolExecutionError(f"Failed to send request to LinkedIn API: {e}")
return response
def _handle_linkedin_api_error(response: httpx.Response):
"""
Handle errors from the LinkedIn API by mapping common status codes to ToolExecutionErrors.
Args:
response: The response object from the API request.
Raises:
ToolExecutionError: If the response contains an error status code.
"""
status_code_map = {
401: ToolExecutionError("Unauthorized: Invalid or expired token"),
403: ToolExecutionError("Forbidden: User does not have Spotify Premium"),
429: ToolExecutionError("Too Many Requests: Rate limit exceeded"),
}
if response.status_code in status_code_map:
raise status_code_map[response.status_code]
elif response.status_code >= 400:
raise ToolExecutionError(f"Error: {response.status_code} - {response.text}")
@tool(
requires_auth=LinkedIn(
scopes=["w_member_social"],
)
)
async def create_text_post(
context: ToolContext,
text: Annotated[str, "The text content of the post"],
) -> Annotated[str, "URL of the shared post"]:
"""Share a new text post to LinkedIn."""
endpoint = "/ugcPosts"
# The LinkedIn user ID is required to create a post, even though we're using the user's access token.
# Arcade Engine gets the current user's info from LinkedIn and automatically populates context.authorization.user_info.
# LinkedIn calls the user ID "sub" in their user_info data payload. See:
# https://learn.microsoft.com/en-us/linkedin/consumer/integrations/self-serve/sign-in-with-linkedin-v2#api-request-to-retreive-member-details
user_id = context.authorization.user_info.get("sub")
if not user_id:
raise ToolExecutionError(
"User ID not found.",
developer_message="User ID not found in `context.authorization.user_info.sub`",
)
author_id = f"urn:li:person:{user_id}"
payload = {
"author": author_id,
"lifecycleState": "PUBLISHED",
"specificContent": {
"com.linkedin.ugc.ShareContent": {
"shareCommentary": {"text": text},
"shareMediaCategory": "NONE",
}
},
"visibility": {"com.linkedin.ugc.MemberNetworkVisibility": "PUBLIC"},
}
response = await _send_linkedin_request(context, "POST", endpoint, json_data=payload)
if response.status_code >= 200 and response.status_code < 300:
share_id = response.json().get("id")
return f"https://www.linkedin.com/feed/update/{share_id}/"
else:
_handle_linkedin_api_error(response)