Addresses general improvements to all toolkits including changing ruff from python 3.9 to python 3.10 which is the reason for the removal of Optional[] among others. Also, turns out that our `make install` for toolkits wasn't correctly checking for whether poetry was installed (&> /dev/null syntax isn't supported by our check-toolkits GitHub action, so we were installing poetry twice. I replaced with the more portable >/dev/null 2>&1) Question: Should we also change ruff to py310 for the `arcade/` package in a later PR? ------------------- CU-86b4gzyp6
162 lines
5.3 KiB
Python
162 lines
5.3 KiB
Python
from typing import Any
|
|
|
|
from arcade.sdk import ToolContext
|
|
from arcade.sdk.errors import ToolExecutionError
|
|
|
|
|
|
def get_tweet_url(tweet_id: str) -> str:
|
|
"""Get the URL of a tweet given its ID."""
|
|
return f"https://x.com/x/status/{tweet_id}"
|
|
|
|
|
|
def get_headers_with_token(context: ToolContext) -> dict[str, str]:
|
|
"""Get the headers for a request to the X API."""
|
|
if context.authorization is None or context.authorization.token is None:
|
|
raise ToolExecutionError(
|
|
"Missing Token. Authorization is required to post a tweet.",
|
|
developer_message="Token is not set in the ToolContext.",
|
|
)
|
|
token = (
|
|
context.authorization.token if context.authorization and context.authorization.token else ""
|
|
)
|
|
return {
|
|
"Authorization": f"Bearer {token}",
|
|
"Content-Type": "application/json",
|
|
}
|
|
|
|
|
|
def parse_search_recent_tweets_response(response_data: dict[str, Any]) -> dict[str, Any]:
|
|
"""
|
|
Parses response from the X API search recent tweets endpoint.
|
|
Returns the modified response data with added 'tweet_url', 'author_username', and 'author_name'.
|
|
"""
|
|
if not sanity_check_tweets_data(response_data):
|
|
return {"data": [], "next_token": ""}
|
|
|
|
# Add 'tweet_url' to each tweet
|
|
for tweet in response_data["data"]:
|
|
tweet["tweet_url"] = get_tweet_url(tweet["id"])
|
|
|
|
# Add 'author_username' and 'author_name' to each tweet
|
|
for tweet_data, user_data in zip(
|
|
response_data["data"], response_data["includes"]["users"], strict=False
|
|
):
|
|
tweet_data["author_username"] = user_data["username"]
|
|
tweet_data["author_name"] = user_data["name"]
|
|
|
|
return response_data
|
|
|
|
|
|
def sanity_check_tweets_data(tweets_data: dict[str, Any]) -> bool:
|
|
"""
|
|
Sanity check the tweets data.
|
|
Returns True if the tweets data is valid and contains tweets, False otherwise.
|
|
"""
|
|
if not tweets_data.get("data"):
|
|
return False
|
|
# prefer clarity over appeasing linter here
|
|
if not tweets_data.get("includes", {}).get("users"): # noqa: SIM103
|
|
return False
|
|
return True
|
|
|
|
|
|
def expand_long_tweet(tweet_data: dict[str, Any]) -> None:
|
|
"""Expand a long tweet.
|
|
|
|
For tweets exceeding 280 characters,
|
|
replace the truncated tweet text with the full tweet text.
|
|
"""
|
|
if tweet_data.get("note_tweet"):
|
|
tweet_data["text"] = tweet_data["note_tweet"]["text"]
|
|
del tweet_data["note_tweet"]
|
|
|
|
|
|
def expand_urls_in_tweets(
|
|
tweets_data: list[dict[str, Any]], delete_entities: bool = True
|
|
) -> list[dict[str, Any]]:
|
|
"""
|
|
Returns a new list of tweets with expanded URLs.
|
|
"""
|
|
new_tweets = []
|
|
for tweet_data in tweets_data:
|
|
new_tweet = tweet_data.copy()
|
|
if "entities" in new_tweet and "urls" in new_tweet["entities"]:
|
|
for url_entity in new_tweet["entities"]["urls"]:
|
|
short_url = url_entity["url"]
|
|
expanded_url = url_entity["expanded_url"]
|
|
new_tweet["text"] = new_tweet["text"].replace(short_url, expanded_url)
|
|
|
|
if delete_entities:
|
|
new_tweet.pop("entities", None)
|
|
new_tweets.append(new_tweet)
|
|
return new_tweets
|
|
|
|
|
|
def expand_urls_in_user_description(user_data: dict, delete_entities: bool = True) -> dict:
|
|
"""
|
|
Returns a new user data dict with expanded URLs in the description.
|
|
"""
|
|
new_user_data = user_data.copy()
|
|
description_urls = new_user_data.get("entities", {}).get("description", {}).get("urls", [])
|
|
description = new_user_data.get("description", "")
|
|
for url_info in description_urls:
|
|
t_co_link = url_info["url"]
|
|
expanded_url = url_info["expanded_url"]
|
|
description = description.replace(t_co_link, expanded_url)
|
|
new_user_data["description"] = description
|
|
|
|
if delete_entities:
|
|
new_user_data.pop("entities", None)
|
|
return new_user_data
|
|
|
|
|
|
def expand_urls_in_user_url(user_data: dict, delete_entities: bool = True) -> dict:
|
|
"""
|
|
Returns a new user data dict with expanded URLs in the URL field.
|
|
"""
|
|
new_user_data = user_data.copy()
|
|
url_urls = new_user_data.get("entities", {}).get("url", {}).get("urls", [])
|
|
url = new_user_data.get("url", "")
|
|
for url_info in url_urls:
|
|
t_co_link = url_info["url"]
|
|
expanded_url = url_info["expanded_url"]
|
|
url = url.replace(t_co_link, expanded_url)
|
|
new_user_data["url"] = url
|
|
|
|
if delete_entities:
|
|
new_user_data.pop("entities", None)
|
|
return new_user_data
|
|
|
|
|
|
def remove_none_values(params: dict) -> dict:
|
|
"""
|
|
Remove key/value pairs with None values from a dictionary.
|
|
|
|
Args:
|
|
params: The dictionary to clean
|
|
|
|
Returns:
|
|
A new dictionary with None values removed
|
|
"""
|
|
return {k: v for k, v in params.items() if v is not None}
|
|
|
|
|
|
def expand_attached_media(params: dict) -> dict:
|
|
"""
|
|
Include attached media metadata in the request parameters.
|
|
"""
|
|
params["expansions"] += ",attachments.media_keys"
|
|
params["tweet.fields"] += ",attachments"
|
|
params["media.fields"] = ",".join([
|
|
# media_key, url and type are returned by default, added here for clarity
|
|
"media_key",
|
|
"url",
|
|
"type",
|
|
"duration_ms",
|
|
"height",
|
|
"width",
|
|
"preview_image_url",
|
|
"alt_text",
|
|
"public_metrics",
|
|
])
|
|
return params
|