add twitter credentials with some frontend changes

# Conflicts:
#	autogpt_platform/backend/backend/data/model.py
#	autogpt_platform/backend/pyproject.toml
#	autogpt_platform/frontend/src/components/integrations/credentials-input.tsx
This commit is contained in:
abhi1992002 2024-11-25 21:09:18 +05:30
parent 10865cd736
commit ca26b298b6
46 changed files with 9755 additions and 19 deletions

View File

@ -0,0 +1,75 @@
from typing import Annotated, Any, Literal, Optional, TypedDict
from uuid import uuid4
from pydantic import BaseModel, Field, SecretStr, field_serializer
class _BaseCredentials(BaseModel):
id: str = Field(default_factory=lambda: str(uuid4()))
provider: str
title: Optional[str]
@field_serializer("*")
def dump_secret_strings(value: Any, _info):
if isinstance(value, SecretStr):
return value.get_secret_value()
return value
class OAuth2Credentials(_BaseCredentials):
type: Literal["oauth2"] = "oauth2"
username: Optional[str]
"""Username of the third-party service user that these credentials belong to"""
access_token: SecretStr
access_token_expires_at: Optional[int]
"""Unix timestamp (seconds) indicating when the access token expires (if at all)"""
refresh_token: Optional[SecretStr]
refresh_token_expires_at: Optional[int]
"""Unix timestamp (seconds) indicating when the refresh token expires (if at all)"""
scopes: list[str]
metadata: dict[str, Any] = Field(default_factory=dict)
def bearer(self) -> str:
return f"Bearer {self.access_token.get_secret_value()}"
class APIKeyCredentials(_BaseCredentials):
type: Literal["api_key"] = "api_key"
api_key: SecretStr
expires_at: Optional[int]
"""Unix timestamp (seconds) indicating when the API key expires (if at all)"""
def bearer(self) -> str:
return f"Bearer {self.api_key.get_secret_value()}"
Credentials = Annotated[
OAuth2Credentials | APIKeyCredentials,
Field(discriminator="type"),
]
CredentialsType = Literal["api_key", "oauth2"]
class OAuthState(BaseModel):
token: str
provider: str
expires_at: int
code_verifier: Optional[str] = None
scopes: list[str]
"""Unix timestamp (seconds) indicating when this OAuth state expires"""
class UserMetadata(BaseModel):
integration_credentials: list[Credentials] = Field(default_factory=list)
integration_oauth_states: list[OAuthState] = Field(default_factory=list)
class UserMetadataRaw(TypedDict, total=False):
integration_credentials: list[dict]
integration_oauth_states: list[dict]
class UserIntegrations(BaseModel):
credentials: list[Credentials] = Field(default_factory=list)
oauth_states: list[OAuthState] = Field(default_factory=list)

View File

@ -58,6 +58,11 @@ GITHUB_CLIENT_SECRET=
GOOGLE_CLIENT_ID=
GOOGLE_CLIENT_SECRET=
# Twitter/X OAuth App server credentials - https://developer.x.com/en/products/x-api
Twitter_CLIENT_ID=
Twitter_CLIENT_SECRET=
## ===== OPTIONAL API KEYS ===== ##
# LLM

View File

@ -0,0 +1,56 @@
from typing import Literal
from autogpt_libs.supabase_integration_credentials_store.types import OAuth2Credentials
from backend.integrations.oauth.twitter import TwitterOAuthHandler
from pydantic import SecretStr
from backend.data.model import CredentialsField, CredentialsMetaInput
from backend.util.settings import Secrets
# --8<-- [start:TwitterOAuthIsConfigured]
secrets = Secrets()
TWITTER_OAUTH_IS_CONFIGURED = bool(
secrets.twitter_client_id and secrets.twitter_client_secret
)
# --8<-- [end:TwitterOAuthIsConfigured]
TwitterCredentials = OAuth2Credentials
TwitterCredentialsInput = CredentialsMetaInput[Literal["twitter"], Literal["oauth2"]]
# Currently, We are getting all the permission from the Twitter API initally
# In future, If we need to add incremental permission, we can use these requested_scopes
def TwitterCredentialsField(scopes: list[str]) -> TwitterCredentialsInput:
"""
Creates a Twitter credentials input on a block.
Params:
scopes: The authorization scopes needed for the block to work.
"""
return CredentialsField(
provider="twitter",
supported_credential_types={"oauth2"},
# required_scopes=set(scopes),
required_scopes=set(TwitterOAuthHandler.DEFAULT_SCOPES),
description="The Twitter integration requires OAuth2 authentication.",
)
TEST_CREDENTIALS = OAuth2Credentials(
id="01234567-89ab-cdef-0123-456789abcdef",
provider="twitter",
access_token=SecretStr("mock-twitter-access-token"),
refresh_token=SecretStr("mock-twitter-refresh-token"),
access_token_expires_at=1234567890,
scopes=["tweet.read", "tweet.write", "users.read", "offline.access"],
title="Mock Twitter OAuth2 Credentials",
username="mock-twitter-username",
refresh_token_expires_at=1234567890,
)
TEST_CREDENTIALS_INPUT = {
"provider": TEST_CREDENTIALS.provider,
"id": TEST_CREDENTIALS.id,
"type": TEST_CREDENTIALS.type,
"title": TEST_CREDENTIALS.title,
}

View File

@ -0,0 +1,264 @@
from typing import Any, Dict
from backend.blocks.twitter._types import TweetExpansions, TweetReplySettings
from backend.blocks.twitter._mappers import get_backend_expansion, get_backend_field, get_backend_list_expansion, get_backend_list_field, get_backend_media_field, get_backend_place_field, get_backend_poll_field, get_backend_space_expansion, get_backend_space_field, get_backend_user_field
# Common Builder
class TweetExpansionsBuilder:
def __init__(self, param : Dict[str, Any]):
self.params: Dict[str, Any] = param
def add_expansions(self, expansions: list[TweetExpansions]):
if expansions:
self.params["expansions"] = ",".join([get_backend_expansion(exp.name) for exp in expansions])
return self
def add_media_fields(self, media_fields: list):
if media_fields:
self.params["media.fields"] = ",".join([get_backend_media_field(field.name) for field in media_fields])
return self
def add_place_fields(self, place_fields: list):
if place_fields:
self.params["place.fields"] = ",".join([get_backend_place_field(field.name) for field in place_fields])
return self
def add_poll_fields(self, poll_fields: list):
if poll_fields:
self.params["poll.fields"] = ",".join([get_backend_poll_field(field.name) for field in poll_fields])
return self
def add_tweet_fields(self, tweet_fields: list):
if tweet_fields:
self.params["tweet.fields"] = ",".join([get_backend_field(field.name) for field in tweet_fields])
return self
def add_user_fields(self, user_fields: list):
if user_fields:
self.params["user.fields"] = ",".join([get_backend_user_field(field.name) for field in user_fields])
return self
def build(self):
return self.params
class UserExpansionsBuilder:
def __init__(self, param : Dict[str, Any]):
self.params: Dict[str, Any] = param
def add_expansions(self, expansions: list):
if expansions:
self.params["expansions"] = ",".join([exp.value for exp in expansions])
return self
def add_tweet_fields(self, tweet_fields: list):
if tweet_fields:
self.params["tweet.fields"] = ",".join([get_backend_field(field.name) for field in tweet_fields])
return self
def add_user_fields(self, user_fields: list):
if user_fields:
self.params["user.fields"] = ",".join([get_backend_user_field(field.name) for field in user_fields])
return self
def build(self):
return self.params
class ListExpansionsBuilder:
def __init__(self, param : Dict[str, Any]):
self.params: Dict[str, Any] = param
def add_expansions(self, expansions: list):
if expansions:
self.params["expansions"] = ",".join([get_backend_list_expansion(exp.name) for exp in expansions])
return self
def add_list_fields(self, list_fields: list):
if list_fields:
self.params["list.fields"] = ",".join([get_backend_list_field(field.name) for field in list_fields])
return self
def add_user_fields(self, user_fields: list):
if user_fields:
self.params["user.fields"] = ",".join([get_backend_user_field(field.name) for field in user_fields])
return self
def build(self):
return self.params
class SpaceExpansionsBuilder:
def __init__(self, param : Dict[str, Any]):
self.params: Dict[str, Any] = param
def add_expansions(self, expansions: list):
if expansions:
self.params["expansions"] = ",".join([get_backend_space_expansion(exp.name) for exp in expansions])
return self
def add_space_fields(self, space_fields: list):
if space_fields:
self.params["space.fields"] = ",".join([get_backend_space_field(field.name) for field in space_fields])
return self
def add_user_fields(self, user_fields: list):
if user_fields:
self.params["user.fields"] = ",".join([get_backend_user_field(field.name) for field in user_fields])
return self
def build(self):
return self.params
class TweetDurationBuilder:
def __init__(self, param : Dict[str, Any]):
self.params: Dict[str, Any] = param
def add_start_time(self, start_time: str):
if start_time:
self.params["start_time"] = start_time
return self
def add_end_time(self, end_time: str):
if end_time:
self.params["end_time"] = end_time
return self
def add_since_id(self, since_id: str):
if since_id:
self.params["since_id"] = since_id
return self
def add_until_id(self, until_id: str):
if until_id:
self.params["until_id"] = until_id
return self
def add_sort_order(self, sort_order: str):
if sort_order:
self.params["sort_order"] = sort_order
return self
def build(self):
return self.params
class DMExpansionsBuilder:
def __init__(self, param : Dict[str, Any]):
self.params: Dict[str, Any] = param
def add_expansions(self, expansions: list):
if expansions:
self.params["expansions"] = ",".join([exp.value for exp in expansions])
return self
def add_event_types(self, event_types: list):
if event_types:
self.params["event_types"] = ",".join([field.value for field in event_types])
return self
def add_media_fields(self, media_fields: list):
if media_fields:
self.params["media.fields"] = ",".join([field.value for field in media_fields])
return self
def add_tweet_fields(self, tweet_fields: list):
if tweet_fields:
self.params["tweet.fields"] = ",".join([field.value for field in tweet_fields])
return self
def add_user_fields(self, user_fields: list):
if user_fields:
self.params["user.fields"] = ",".join([field.value for field in user_fields])
return self
def build(self):
return self.params
# Specific Builders
class TweetSearchBuilder:
def __init__(self):
self.params: Dict[str, Any] = {"user_auth": False}
def add_query(self, query: str):
if query:
self.params["query"] = query
return self
def add_pagination(self, max_results: int, pagination: str):
if max_results:
self.params["max_results"] = max_results
if pagination:
self.params["pagination_token"] = pagination
return self
def build(self):
return self.params
class TweetPostBuilder:
def __init__(self):
self.params: Dict[str, Any] = {"user_auth": False}
def add_text(self, text: str):
if text:
self.params["text"] = text
return self
def add_media(self, media_ids: list, tagged_user_ids: list):
if media_ids:
self.params["media_ids"] = media_ids
if tagged_user_ids:
self.params["media_tagged_user_ids"] = tagged_user_ids
return self
def add_deep_link(self, link: str):
if link:
self.params["direct_message_deep_link"] = link
return self
def add_super_followers(self, for_super_followers: bool):
if for_super_followers:
self.params["for_super_followers_only"] = for_super_followers
return self
def add_place(self, place_id: str):
if place_id:
self.params["place_id"] = place_id
return self
def add_poll_options(self, poll_options: list):
if poll_options:
self.params["poll_options"] = poll_options
return self
def add_poll_duration(self, poll_duration_minutes: int ):
if poll_duration_minutes:
self.params["poll_duration_minutes"] = poll_duration_minutes
return self
def add_quote(self, quote_id: str):
if quote_id:
self.params["quote_tweet_id"] = quote_id
return self
def add_reply_settings(self, exclude_user_ids: list, reply_to_id: str, settings: TweetReplySettings):
if exclude_user_ids:
self.params["exclude_reply_user_ids"] = exclude_user_ids
if reply_to_id:
self.params["in_reply_to_tweet_id"] = reply_to_id
if settings == TweetReplySettings.all_users:
self.params["reply_settings"] = None
else:
self.params["reply_settings"] = settings
return self
def build(self):
return self.params
class TweetGetsBuilder:
def __init__(self):
self.params: Dict[str, Any] = {"user_auth": False}
def add_id(self, tweet_id: list[str]):
self.params["id"] = tweet_id
return self
def build(self):
return self.params

View File

@ -0,0 +1,215 @@
# -------------- Tweets -----------------
# Tweet Expansions
EXPANSION_FRONTEND_TO_BACKEND_MAPPING = {
"attachments_poll_ids": "attachments.poll_ids",
"attachments_media_keys": "attachments.media_keys",
"author_id": "author_id",
"edit_history_tweet_ids": "edit_history_tweet_ids",
"entities_mentions_username": "entities.mentions.username",
"geo_place_id": "geo.place_id",
"in_reply_to_user_id": "in_reply_to_user_id",
"referenced_tweets_id": "referenced_tweets.id",
"referenced_tweets_id_author_id": "referenced_tweets.id.author_id",
}
def get_backend_expansion(frontend_key: str) -> str:
result = EXPANSION_FRONTEND_TO_BACKEND_MAPPING.get(frontend_key)
if result is None:
raise KeyError(f"Invalid expansion key: {frontend_key}")
return result
# TweetReplySettings
REPLY_SETTINGS_FRONTEND_TO_BACKEND_MAPPING = {
"mentioned_users": "mentionedUsers",
"following": "following",
"all_users": "all"
}
# TweetUserFields
def get_backend_reply_setting(frontend_key: str) -> str:
result = REPLY_SETTINGS_FRONTEND_TO_BACKEND_MAPPING.get(frontend_key)
if result is None:
raise KeyError(f"Invalid reply setting key: {frontend_key}")
return result
USER_FIELDS_FRONTEND_TO_BACKEND_MAPPING = {
"created_at": "created_at",
"description": "description",
"entities": "entities",
"id": "id",
"location": "location",
"most_recent_tweet_id": "most_recent_tweet_id",
"name_user": "name",
"pinned_tweet_id": "pinned_tweet_id",
"profile_image_url": "profile_image_url",
"protected": "protected",
"public_metrics": "public_metrics",
"url": "url",
"username": "username",
"verified": "verified",
"verified_type": "verified_type",
"withheld": "withheld"
}
def get_backend_user_field(frontend_key: str) -> str:
result = USER_FIELDS_FRONTEND_TO_BACKEND_MAPPING.get(frontend_key)
if result is None:
raise KeyError(f"Invalid user field key: {frontend_key}")
return result
# TweetFields
FIELDS_FRONTEND_TO_BACKEND_MAPPING = {
"attachments": "attachments",
"author_id": "author_id",
"context_annotations": "context_annotations",
"conversation_id": "conversation_id",
"created_at": "created_at",
"edit_controls": "edit_controls",
"entities": "entities",
"geo": "geo",
"id": "id",
"in_reply_to_user_id": "in_reply_to_user_id",
"lang": "lang",
"public_metrics": "public_metrics",
"possibly_sensitive": "possibly_sensitive",
"referenced_tweets": "referenced_tweets",
"reply_settings": "reply_settings",
"source": "source",
"text": "text",
"withheld": "withheld"
}
def get_backend_field(frontend_key: str) -> str:
result = FIELDS_FRONTEND_TO_BACKEND_MAPPING.get(frontend_key)
if result is None:
raise KeyError(f"Invalid field key: {frontend_key}")
return result
# TweetPollFields
POLL_FIELDS_FRONTEND_TO_BACKEND_MAPPING = {
"duration_minutes": "duration_minutes",
"end_datetime": "end_datetime",
"id": "id",
"options": "options",
"voting_status": "voting_status"
}
def get_backend_poll_field(frontend_key: str) -> str:
result = POLL_FIELDS_FRONTEND_TO_BACKEND_MAPPING.get(frontend_key)
if result is None:
raise KeyError(f"Invalid poll field key: {frontend_key}")
return result
PLACE_FIELDS_FRONTEND_TO_BACKEND_MAPPING = {
"contained_within": "contained_within",
"country": "country",
"country_code": "country_code",
"full_name": "full_name",
"geo": "geo",
"id": "id",
"place_name": "name",
"place_type": "place_type"
}
def get_backend_place_field(frontend_key: str) -> str:
result = PLACE_FIELDS_FRONTEND_TO_BACKEND_MAPPING.get(frontend_key)
if result is None:
raise KeyError(f"Invalid place field key: {frontend_key}")
return result
# TweetMediaFields
MEDIA_FIELDS_FRONTEND_TO_BACKEND_MAPPING = {
"duration_ms": "duration_ms",
"height": "height",
"media_key": "media_key",
"preview_image_url": "preview_image_url",
"type": "type",
"url": "url",
"width": "width",
"public_metrics": "public_metrics",
"non_public_metrics": "non_public_metrics",
"organic_metrics": "organic_metrics",
"promoted_metrics": "promoted_metrics",
"alt_text": "alt_text",
"variants": "variants"
}
def get_backend_media_field(frontend_key: str) -> str:
result = MEDIA_FIELDS_FRONTEND_TO_BACKEND_MAPPING.get(frontend_key)
if result is None:
raise KeyError(f"Invalid media field key: {frontend_key}")
return result
# -------------- Spaces -----------------
# SpaceExpansions
EXPANSION_FRONTEND_TO_BACKEND_MAPPING_SPACE = {
"invited_user_ids": "invited_user_ids",
"speaker_ids": "speaker_ids",
"creator_id": "creator_id",
"host_ids": "host_ids",
"topic_ids": "topic_ids"
}
def get_backend_space_expansion(frontend_key: str) -> str:
result = EXPANSION_FRONTEND_TO_BACKEND_MAPPING_SPACE.get(frontend_key)
if result is None:
raise KeyError(f"Invalid expansion key: {frontend_key}")
return result
# SpaceFields
SPACE_FIELDS_FRONTEND_TO_BACKEND_MAPPING = {
"id": "id",
"state": "state",
"created_at": "created_at",
"ended_at": "ended_at",
"host_ids": "host_ids",
"lang": "lang",
"is_ticketed": "is_ticketed",
"invited_user_ids": "invited_user_ids",
"participant_count": "participant_count",
"subscriber_count": "subscriber_count",
"scheduled_start": "scheduled_start",
"speaker_ids": "speaker_ids",
"started_at": "started_at",
"title_": "title",
"topic_ids": "topic_ids",
"updated_at": "updated_at"
}
def get_backend_space_field(frontend_key: str) -> str:
result = SPACE_FIELDS_FRONTEND_TO_BACKEND_MAPPING.get(frontend_key)
if result is None:
raise KeyError(f"Invalid space field key: {frontend_key}")
return result
# -------------- List Expansions -----------------
# ListExpansions
LIST_EXPANSION_FRONTEND_TO_BACKEND_MAPPING = {
"owner_id": "owner_id"
}
def get_backend_list_expansion(frontend_key: str) -> str:
result = LIST_EXPANSION_FRONTEND_TO_BACKEND_MAPPING.get(frontend_key)
if result is None:
raise KeyError(f"Invalid list expansion key: {frontend_key}")
return result
LIST_FIELDS_FRONTEND_TO_BACKEND_MAPPING = {
"id": "id",
"list_name": "name",
"created_at": "created_at",
"description": "description",
"follower_count": "follower_count",
"member_count": "member_count",
"private": "private",
"owner_id": "owner_id"
}
def get_backend_list_field(frontend_key: str) -> str:
result = LIST_FIELDS_FRONTEND_TO_BACKEND_MAPPING.get(frontend_key)
if result is None:
raise KeyError(f"Invalid list field key: {frontend_key}")
return result

View File

@ -0,0 +1,75 @@
from typing import Any, Dict, List
class BaseSerializer:
@staticmethod
def _serialize_value(value: Any) -> Any:
"""Helper method to serialize individual values"""
if hasattr(value, 'data'):
return value.data
return value
class IncludesSerializer(BaseSerializer):
@classmethod
def serialize(cls, includes: Dict[str, Any]) -> Dict[str, Any]:
"""Serializes the includes dictionary"""
if not includes:
return {}
serialized_includes = {}
for key, value in includes.items():
if isinstance(value, list):
serialized_includes[key] = [
cls._serialize_value(item) for item in value
]
else:
serialized_includes[key] = cls._serialize_value(value)
return serialized_includes
class ResponseDataSerializer(BaseSerializer):
@classmethod
def serialize_dict(cls, item: Dict[str, Any]) -> Dict[str, Any]:
"""Serializes a single dictionary item"""
serialized_item = {}
if hasattr(item, '__dict__'):
items = item.__dict__.items()
else:
items = item.items()
for key, value in items:
if isinstance(value, list):
serialized_item[key] = [
cls._serialize_value(sub_item) for sub_item in value
]
else:
serialized_item[key] = cls._serialize_value(value)
return serialized_item
@classmethod
def serialize_list(cls, data: List[Dict[str, Any]]) -> List[Dict[str, Any]]:
"""Serializes a list of dictionary items"""
return [cls.serialize_dict(item) for item in data]
class ResponseSerializer:
@classmethod
def serialize(cls, response) -> Dict[str, Any]:
"""Main serializer that handles both data and includes"""
result = {
'data': None,
'included': {}
}
# Handle response.data
if response.data:
if isinstance(response.data, list):
result['data'] = ResponseDataSerializer.serialize_list(response.data)
else:
result['data'] = ResponseDataSerializer.serialize_dict(response.data)
# Handle includes
if hasattr(response, 'includes') and response.includes:
result['included'] = IncludesSerializer.serialize(response.includes)
return result

View File

@ -0,0 +1,447 @@
from enum import Enum
from backend.data.block import BlockSchema
from backend.data.model import SchemaField
# -------------- Tweets -----------------
class TweetReplySettings(str, Enum):
mentioned_users = "Mentioned_Users_Only"
following = "Following_Users_Only"
all_users = "All_Users"
class TweetUserFields(str, Enum):
created_at = "Account_Creation_Date"
description = "User_Bio"
entities = "User_Entities"
id = "User_ID"
location = "User_Location"
most_recent_tweet_id = "Latest_Tweet_ID"
name_user = "Display_Name"
pinned_tweet_id = "Pinned_Tweet_ID"
profile_image_url = "Profile_Picture_URL"
protected = "Is_Protected_Account"
public_metrics = "Account_Statistics"
url = "Profile_URL"
username = "Username"
verified = "Is_Verified"
verified_type = "Verification_Type"
withheld = "Content_Withholding_Info"
class TweetFields(str, Enum):
attachments = "Tweet_Attachments"
author_id = "Author_ID"
context_annotations = "Context_Annotations"
conversation_id = "Conversation_ID"
created_at = "Creation_Time"
edit_controls = "Edit_Controls"
entities = "Tweet_Entities"
geo = "Geographic_Location"
id = "Tweet_ID"
in_reply_to_user_id = "Reply_To_User_ID"
lang = "Language"
public_metrics = "Public_Metrics"
possibly_sensitive = "Sensitive_Content_Flag"
referenced_tweets = "Referenced_Tweets"
reply_settings = "Reply_Settings"
source = "Tweet_Source"
text = "Tweet_Text"
withheld = "Withheld_Content"
class PersonalTweetFields(str, Enum):
attachments = "attachments"
author_id = "author_id"
context_annotations = "context_annotations"
conversation_id = "conversation_id"
created_at = "created_at"
edit_controls = "edit_controls"
entities = "entities"
geo = "geo"
id = "id"
in_reply_to_user_id = "in_reply_to_user_id"
lang = "lang"
non_public_metrics = "non_public_metrics"
public_metrics = "public_metrics"
organic_metrics = "organic_metrics"
promoted_metrics = "promoted_metrics"
possibly_sensitive = "possibly_sensitive"
referenced_tweets = "referenced_tweets"
reply_settings = "reply_settings"
source = "source"
text = "text"
withheld = "withheld"
class TweetPollFields(str, Enum):
duration_minutes = "Duration_Minutes"
end_datetime = "End_DateTime"
id = "Poll_ID"
options = "Poll_Options"
voting_status = "Voting_Status"
class TweetPlaceFields(str, Enum):
contained_within = "Contained_Within_Places"
country = "Country"
country_code = "Country_Code"
full_name = "Full_Location_Name"
geo = "Geographic_Coordinates"
id = "Place_ID"
place_name = "Place_Name"
place_type = "Place_Type"
class TweetMediaFields(str, Enum):
duration_ms = "Duration_in_Milliseconds"
height = "Height"
media_key = "Media_Key"
preview_image_url = "Preview_Image_URL"
type = "Media_Type"
url = "Media_URL"
width = "Width"
public_metrics = "Public_Metrics"
non_public_metrics = "Non_Public_Metrics"
organic_metrics = "Organic_Metrics"
promoted_metrics = "Promoted_Metrics"
alt_text = "Alternative_Text"
variants = "Media_Variants"
class TweetExpansions(str, Enum):
attachments_poll_ids = "Poll_IDs"
attachments_media_keys = "Media_Keys"
author_id = "Author_User_ID"
edit_history_tweet_ids = "Edit_History_Tweet_IDs"
entities_mentions_username = "Mentioned_Usernames"
geo_place_id = "Place_ID"
in_reply_to_user_id = "Reply_To_User_ID"
referenced_tweets_id = "Referenced_Tweet_ID"
referenced_tweets_id_author_id = "Referenced_Tweet_Author_ID"
class TweetExcludes(str, Enum):
retweets = "retweets"
replies = "replies"
# -------------- Users -----------------
class UserExpansions(str, Enum):
pinned_tweet_id = "pinned_tweet_id"
# -------------- DM's' -----------------
class DMEventField(str, Enum):
ID = "id"
TEXT = "text"
EVENT_TYPE = "event_type"
CREATED_AT = "created_at"
DM_CONVERSATION_ID = "dm_conversation_id"
SENDER_ID = "sender_id"
PARTICIPANT_IDS = "participant_ids"
REFERENCED_TWEETS = "referenced_tweets"
ATTACHMENTS = "attachments"
class DMEventType(str, Enum):
MESSAGE_CREATE = "MessageCreate"
PARTICIPANTS_JOIN = "ParticipantsJoin"
PARTICIPANTS_LEAVE = "ParticipantsLeave"
class DMEventExpansion(str, Enum):
ATTACHMENTS_MEDIA_KEYS = "attachments.media_keys"
REFERENCED_TWEETS_ID = "referenced_tweets.id"
SENDER_ID = "sender_id"
PARTICIPANT_IDS = "participant_ids"
class DMMediaField(str, Enum):
DURATION_MS = "duration_ms"
HEIGHT = "height"
MEDIA_KEY = "media_key"
PREVIEW_IMAGE_URL = "preview_image_url"
TYPE = "type"
URL = "url"
WIDTH = "width"
PUBLIC_METRICS = "public_metrics"
ALT_TEXT = "alt_text"
VARIANTS = "variants"
class DMTweetField(str, Enum):
ATTACHMENTS = "attachments"
AUTHOR_ID = "author_id"
CONTEXT_ANNOTATIONS = "context_annotations"
CONVERSATION_ID = "conversation_id"
CREATED_AT = "created_at"
EDIT_CONTROLS = "edit_controls"
ENTITIES = "entities"
GEO = "geo"
ID = "id"
IN_REPLY_TO_USER_ID = "in_reply_to_user_id"
LANG = "lang"
PUBLIC_METRICS = "public_metrics"
POSSIBLY_SENSITIVE = "possibly_sensitive"
REFERENCED_TWEETS = "referenced_tweets"
REPLY_SETTINGS = "reply_settings"
SOURCE = "source"
TEXT = "text"
WITHHELD = "withheld"
# -------------- Spaces -----------------
class SpaceExpansions(str, Enum):
invited_user_ids = "Invited_Users"
speaker_ids = "Speakers"
creator_id = "Creator"
host_ids = "Hosts"
topic_ids = "Topics"
class SpaceFields(str, Enum):
id = "Space_ID"
state = "Space_State"
created_at = "Creation_Time"
ended_at = "End_Time"
host_ids = "Host_User_IDs"
lang = "Language"
is_ticketed = "Is_Ticketed"
invited_user_ids = "Invited_User_IDs"
participant_count = "Participant_Count"
subscriber_count = "Subscriber_Count"
scheduled_start = "Scheduled_Start_Time"
speaker_ids = "Speaker_User_IDs"
started_at = "Start_Time"
title_ = "Space_Title"
topic_ids = "Topic_IDs"
updated_at = "Last_Updated_Time"
class SpaceStates(str, Enum):
LIVE = "live"
SCHEDULED = "scheduled"
ALL = "all"
# -------------- List Expansions -----------------
class ListExpansions(str, Enum):
owner_id = "List_Owner_ID"
class ListFields(str, Enum):
id = "List_ID"
list_name = "List_Name"
created_at = "Creation_Date"
description = "Description"
follower_count = "Follower_Count"
member_count = "Member_Count"
private = "Is_Private"
owner_id = "Owner_ID"
# --------- [Input Types] -------------
class TweetExpansionInputs(BlockSchema):
expansions: list[TweetExpansions] = SchemaField(
description="Choose what extra information you want to get with your tweets. For example:\n- Select 'Media_Keys' to get media details\n- Select 'Author_User_ID' to get user information\n- Select 'Place_ID' to get location details",
enum = TweetExpansions,
placeholder="Pick the extra information you want to see",
default=[],
is_multi_select=True,
advanced=True,
)
media_fields: list[TweetMediaFields] = SchemaField(
description="Select what media information you want to see (images, videos, etc). To use this, you must first select 'Media_Keys' in the expansions above.",
enum = TweetMediaFields,
placeholder="Choose what media details you want to see",
default=[],
is_multi_select=True,
advanced=True
)
place_fields: list[TweetPlaceFields] = SchemaField(
description="Select what location information you want to see (country, coordinates, etc). To use this, you must first select 'Place_ID' in the expansions above.",
placeholder="Choose what location details you want to see",
default=[],
advanced=True,
is_multi_select=True,
enum = TweetPlaceFields
)
poll_fields: list[TweetPollFields] = SchemaField(
description="Select what poll information you want to see (options, voting status, etc). To use this, you must first select 'Poll_IDs' in the expansions above.",
placeholder="Choose what poll details you want to see",
default=[],
advanced=True,
is_multi_select=True,
enum = TweetPollFields
)
tweet_fields: list[TweetFields] = SchemaField(
description="Select what tweet information you want to see. For referenced tweets (like retweets), select 'Referenced_Tweet_ID' in the expansions above.",
placeholder="Choose what tweet details you want to see",
default=[],
advanced=True,
is_multi_select=True,
enum = TweetFields
)
user_fields: list[TweetUserFields] = SchemaField(
description="Select what user information you want to see. To use this, you must first select one of these in expansions above:\n- 'Author_User_ID' for tweet authors\n- 'Mentioned_Usernames' for mentioned users\n- 'Reply_To_User_ID' for users being replied to\n- 'Referenced_Tweet_Author_ID' for authors of referenced tweets",
placeholder="Choose what user details you want to see",
default=[],
advanced=True,
is_multi_select=True,
enum = TweetUserFields
)
class DMEventExpansionInputs(BlockSchema):
expansions: list[DMEventExpansion] = SchemaField(
description="Select expansions to include related data objects in the 'includes' section.",
enum = DMEventExpansion,
placeholder="Enter expansions",
default=[],
is_multi_select=True,
advanced=True,
)
event_types: list[DMEventType] = SchemaField(
description="Select DM event types to include in the response.",
placeholder="Enter event types",
default=[],
advanced=True,
is_multi_select=True,
enum = DMEventType
)
media_fields: list[DMMediaField] = SchemaField(
description="Select media fields to include in the response (requires expansions=attachments.media_keys).",
placeholder="Enter media fields",
default=[],
advanced=True,
is_multi_select=True,
enum = DMMediaField
)
tweet_fields: list[DMTweetField] = SchemaField(
description="Select tweet fields to include in the response (requires expansions=referenced_tweets.id).",
placeholder="Enter tweet fields",
default=[],
advanced=True,
is_multi_select=True,
enum = DMTweetField
)
user_fields: list[TweetUserFields] = SchemaField(
description="Select user fields to include in the response (requires expansions=sender_id or participant_ids).",
placeholder="Enter user fields",
default=[],
advanced=True,
is_multi_select=True,
enum = TweetUserFields
)
class UserExpansionInputs(BlockSchema):
expansions: list[UserExpansions] = SchemaField(
description="Choose what extra information you want to get with user data. Currently only 'pinned_tweet_id' is available to see a user's pinned tweet.",
enum = UserExpansions,
placeholder="Select extra user information to include",
default=[],
is_multi_select=True,
advanced=True,
)
tweet_fields: list[TweetFields] = SchemaField(
description="Select what tweet information you want to see in pinned tweets. This only works if you select 'pinned_tweet_id' in expansions above.",
placeholder="Choose what details to see in pinned tweets",
default=[],
advanced=True,
is_multi_select=True,
enum = TweetFields
)
user_fields: list[TweetUserFields] = SchemaField(
description="Select what user information you want to see, like username, bio, profile picture, etc.",
placeholder="Choose what user details you want to see",
default=[],
advanced=True,
is_multi_select=True,
enum = TweetUserFields
)
class SpaceExpansionInputs(BlockSchema):
expansions: list[SpaceExpansions] = SchemaField(
description="Choose additional information you want to get with your Twitter Spaces:\n- Select 'Invited_Users' to see who was invited\n- Select 'Speakers' to see who can speak\n- Select 'Creator' to get details about who made the Space\n- Select 'Hosts' to see who's hosting\n- Select 'Topics' to see Space topics",
enum = SpaceExpansions,
placeholder="Pick what extra information you want to see about the Space",
default=[],
is_multi_select=True,
advanced=True,
)
space_fields: list[SpaceFields] = SchemaField(
description="Choose what Space details you want to see, such as:\n- Title\n- Start/End times\n- Number of participants\n- Language\n- State (live/scheduled)\n- And more",
placeholder="Choose what Space information you want to get",
default=[SpaceFields.title_,SpaceFields.host_ids],
advanced=True,
is_multi_select=True,
enum = SpaceFields
)
user_fields: list[TweetUserFields] = SchemaField(
description="Choose what user information you want to see. This works when you select any of these in expansions above:\n- 'Creator' for Space creator details\n- 'Hosts' for host information\n- 'Speakers' for speaker details\n- 'Invited_Users' for invited user information",
placeholder="Pick what details you want to see about the users",
default=[],
advanced=True,
is_multi_select=True,
enum = TweetUserFields
)
class ListExpansionInputs(BlockSchema):
expansions: list[ListExpansions] = SchemaField(
description="Choose what extra information you want to get with your Twitter Lists:\n- Select 'List_Owner_ID' to get details about who owns the list\n\nThis will let you see more details about the list owner when you also select user fields below.",
enum = ListExpansions,
placeholder="Pick what extra list information you want to see",
default=[ListExpansions.owner_id],
is_multi_select=True,
advanced=True,
)
user_fields: list[TweetUserFields] = SchemaField(
description="Choose what information you want to see about list owners. This only works when you select 'List_Owner_ID' in expansions above.\n\nYou can see things like:\n- Their username\n- Profile picture\n- Account details\n- And more",
placeholder="Select what details you want to see about list owners",
default=[TweetUserFields.id,TweetUserFields.username],
advanced=True,
is_multi_select=True,
enum = TweetUserFields
)
list_fields: list[ListFields] = SchemaField(
description="Choose what information you want to see about the Twitter Lists themselves, such as:\n- List name\n- Description\n- Number of followers\n- Number of members\n- Whether it's private\n- Creation date\n- And more",
placeholder="Pick what list details you want to see",
default=[ListFields.owner_id],
advanced=True,
is_multi_select=True,
enum = ListFields
)
class TweetTimeWindowInputs(BlockSchema):
start_time: str = SchemaField(
description="Start time in YYYY-MM-DDTHH:mm:ssZ format",
placeholder="Enter start time",
default="",
)
end_time: str = SchemaField(
description="End time in YYYY-MM-DDTHH:mm:ssZ format",
default="",
placeholder="Enter end time",
)
since_id: str = SchemaField(
description="Returns results with Tweet ID greater than this (more recent than), we give priority to since_id over start_time",
default="",
placeholder="Enter since ID",
)
until_id: str = SchemaField(
description="Returns results with Tweet ID less than this (that is, older than), and used with since_id",
default="",
placeholder="Enter until ID",
)
sort_order: str = SchemaField(
description="Order of returned tweets (recency or relevancy)",
default="",
placeholder="Enter sort order",
)

View File

@ -0,0 +1,199 @@
# from typing import cast
# import tweepy
# from tweepy.client import Response
# from backend.blocks.twitter._serializer import IncludesSerializer, ResponseDataSerializer
# from backend.data.block import Block, BlockCategory, BlockOutput, BlockSchema
# from backend.data.model import SchemaField
# from backend.blocks.twitter._builders import DMExpansionsBuilder
# from backend.blocks.twitter._types import DMEventExpansion, DMEventExpansionInputs, DMEventType, DMMediaField, DMTweetField, TweetUserFields
# from backend.blocks.twitter.tweepy_exceptions import handle_tweepy_exception
# from backend.blocks.twitter._auth import (
# TEST_CREDENTIALS,
# TEST_CREDENTIALS_INPUT,
# TwitterCredentials,
# TwitterCredentialsField,
# TwitterCredentialsInput,
# )
# Require Pro or Enterprise plan [Manual Testing Required]
# class TwitterGetDMEventsBlock(Block):
# """
# Gets a list of Direct Message events for the authenticated user
# """
# class Input(DMEventExpansionInputs):
# credentials: TwitterCredentialsInput = TwitterCredentialsField(
# ["dm.read", "offline.access", "user.read", "tweet.read"]
# )
# dm_conversation_id: str = SchemaField(
# description="The ID of the Direct Message conversation",
# placeholder="Enter conversation ID",
# required=True
# )
# max_results: int = SchemaField(
# description="Maximum number of results to return (1-100)",
# placeholder="Enter max results",
# advanced=True,
# default=10,
# )
# pagination_token: str = SchemaField(
# description="Token for pagination",
# placeholder="Enter pagination token",
# advanced=True,
# default=""
# )
# class Output(BlockSchema):
# # Common outputs
# event_ids: list[str] = SchemaField(description="DM Event IDs")
# event_texts: list[str] = SchemaField(description="DM Event text contents")
# event_types: list[str] = SchemaField(description="Types of DM events")
# next_token: str = SchemaField(description="Token for next page of results")
# # Complete outputs
# data: list[dict] = SchemaField(description="Complete DM events data")
# included: dict = SchemaField(description="Additional data requested via expansions")
# meta: dict = SchemaField(description="Metadata about the response")
# error: str = SchemaField(description="Error message if request failed")
# def __init__(self):
# super().__init__(
# id="dc37a6d4-a62e-11ef-a3a5-03061375737b",
# description="This block retrieves Direct Message events for the authenticated user.",
# categories={BlockCategory.SOCIAL},
# input_schema=TwitterGetDMEventsBlock.Input,
# output_schema=TwitterGetDMEventsBlock.Output,
# test_input={
# "dm_conversation_id": "1234567890",
# "max_results": 10,
# "credentials": TEST_CREDENTIALS_INPUT,
# "expansions": [],
# "event_types": [],
# "media_fields": [],
# "tweet_fields": [],
# "user_fields": []
# },
# test_credentials=TEST_CREDENTIALS,
# test_output=[
# ("event_ids", ["1346889436626259968"]),
# ("event_texts", ["Hello just you..."]),
# ("event_types", ["MessageCreate"]),
# ("next_token", None),
# ("data", [{"id": "1346889436626259968", "text": "Hello just you...", "event_type": "MessageCreate"}]),
# ("included", {}),
# ("meta", {}),
# ("error", "")
# ],
# test_mock={
# "get_dm_events": lambda *args, **kwargs: (
# [{"id": "1346889436626259968", "text": "Hello just you...", "event_type": "MessageCreate"}],
# {},
# {},
# ["1346889436626259968"],
# ["Hello just you..."],
# ["MessageCreate"],
# None
# )
# }
# )
# @staticmethod
# def get_dm_events(
# credentials: TwitterCredentials,
# dm_conversation_id: str,
# max_results: int,
# pagination_token: str,
# expansions: list[DMEventExpansion],
# event_types: list[DMEventType],
# media_fields: list[DMMediaField],
# tweet_fields: list[DMTweetField],
# user_fields: list[TweetUserFields]
# ):
# try:
# client = tweepy.Client(
# bearer_token=credentials.access_token.get_secret_value()
# )
# params = {
# "dm_conversation_id": dm_conversation_id,
# "max_results": max_results,
# "pagination_token": None if pagination_token == "" else pagination_token,
# "user_auth": False
# }
# params = (DMExpansionsBuilder(params)
# .add_expansions(expansions)
# .add_event_types(event_types)
# .add_media_fields(media_fields)
# .add_tweet_fields(tweet_fields)
# .add_user_fields(user_fields)
# .build())
# response = cast(Response, client.get_direct_message_events(**params))
# meta = {}
# event_ids = []
# event_texts = []
# event_types = []
# next_token = None
# if response.meta:
# meta = response.meta
# next_token = meta.get("next_token")
# included = IncludesSerializer.serialize(response.includes)
# data = ResponseDataSerializer.serialize_list(response.data)
# if response.data:
# event_ids = [str(item.id) for item in response.data]
# event_texts = [item.text if hasattr(item, "text") else None for item in response.data]
# event_types = [item.event_type for item in response.data]
# return data, included, meta, event_ids, event_texts, event_types, next_token
# raise Exception("No DM events found")
# except tweepy.TweepyException:
# raise
# def run(
# self,
# input_data: Input,
# *,
# credentials: TwitterCredentials,
# **kwargs,
# ) -> BlockOutput:
# try:
# event_data, included, meta, event_ids, event_texts, event_types, next_token = self.get_dm_events(
# credentials,
# input_data.dm_conversation_id,
# input_data.max_results,
# input_data.pagination_token,
# input_data.expansions,
# input_data.event_types,
# input_data.media_fields,
# input_data.tweet_fields,
# input_data.user_fields
# )
# if event_ids:
# yield "event_ids", event_ids
# if event_texts:
# yield "event_texts", event_texts
# if event_types:
# yield "event_types", event_types
# if next_token:
# yield "next_token", next_token
# if event_data:
# yield "data", event_data
# if included:
# yield "included", included
# if meta:
# yield "meta", meta
# except Exception as e:
# yield "error", handle_tweepy_exception(e)

View File

@ -0,0 +1,258 @@
# from typing import cast
# import tweepy
# from tweepy.client import Response
# from backend.data.block import Block, BlockCategory, BlockOutput, BlockSchema
# from backend.data.model import SchemaField
# from backend.blocks.twitter.tweepy_exceptions import handle_tweepy_exception
# from backend.blocks.twitter._auth import (
# TEST_CREDENTIALS,
# TEST_CREDENTIALS_INPUT,
# TwitterCredentials,
# TwitterCredentialsField,
# TwitterCredentialsInput,
# )
# Pro and Enterprise plan [Manual Testing Required]
# class TwitterSendDirectMessageBlock(Block):
# """
# Sends a direct message to a Twitter user
# """
# class Input(BlockSchema):
# credentials: TwitterCredentialsInput = TwitterCredentialsField(
# ["offline.access", "direct_messages.write"]
# )
# participant_id: str = SchemaField(
# description="The User ID of the account to send DM to",
# placeholder="Enter recipient user ID",
# default="",
# advanced=False
# )
# dm_conversation_id: str = SchemaField(
# description="The conversation ID to send message to",
# placeholder="Enter conversation ID",
# default="",
# advanced=False
# )
# text: str = SchemaField(
# description="Text of the Direct Message (up to 10,000 characters)",
# placeholder="Enter message text",
# default="",
# advanced=False
# )
# media_id: str = SchemaField(
# description="Media ID to attach to the message",
# placeholder="Enter media ID",
# default=""
# )
# class Output(BlockSchema):
# dm_event_id: str = SchemaField(description="ID of the sent direct message")
# dm_conversation_id_: str = SchemaField(description="ID of the conversation")
# error: str = SchemaField(description="Error message if sending failed")
# def __init__(self):
# super().__init__(
# id="f32f2786-a62e-11ef-a93d-a3ef199dde7f",
# description="This block sends a direct message to a specified Twitter user.",
# categories={BlockCategory.SOCIAL},
# input_schema=TwitterSendDirectMessageBlock.Input,
# output_schema=TwitterSendDirectMessageBlock.Output,
# test_input={
# "participant_id": "783214",
# "dm_conversation_id": "",
# "text": "Hello from Twitter API",
# "media_id": "",
# "credentials": TEST_CREDENTIALS_INPUT
# },
# test_credentials=TEST_CREDENTIALS,
# test_output=[
# ("dm_event_id", "0987654321"),
# ("dm_conversation_id_", "1234567890"),
# ("error", "")
# ],
# test_mock={
# "send_direct_message": lambda *args, **kwargs: (
# "0987654321",
# "1234567890"
# )
# },
# )
# @staticmethod
# def send_direct_message(
# credentials: TwitterCredentials,
# participant_id: str,
# dm_conversation_id: str,
# text: str,
# media_id: str
# ):
# try:
# client = tweepy.Client(
# bearer_token=credentials.access_token.get_secret_value()
# )
# response = cast(
# Response,
# client.create_direct_message(
# participant_id=None if participant_id == "" else participant_id,
# dm_conversation_id=None if dm_conversation_id == "" else dm_conversation_id,
# text=None if text == "" else text,
# media_id=None if media_id == "" else media_id,
# user_auth=False
# )
# )
# if not response.data:
# raise Exception("Failed to send direct message")
# return response.data["dm_event_id"], response.data["dm_conversation_id"]
# except tweepy.TweepyException:
# raise
# except Exception as e:
# print(f"Unexpected error: {str(e)}")
# raise
# def run(
# self,
# input_data: Input,
# *,
# credentials: TwitterCredentials,
# **kwargs,
# ) -> BlockOutput:
# try:
# dm_event_id, dm_conversation_id = self.send_direct_message(
# credentials,
# input_data.participant_id,
# input_data.dm_conversation_id,
# input_data.text,
# input_data.media_id
# )
# yield "dm_event_id", dm_event_id
# yield "dm_conversation_id", dm_conversation_id
# except Exception as e:
# yield "error", handle_tweepy_exception(e)
# class TwitterCreateDMConversationBlock(Block):
# """
# Creates a new group direct message conversation on Twitter
# """
# class Input(BlockSchema):
# credentials: TwitterCredentialsInput = TwitterCredentialsField(
# ["offline.access", "dm.write","dm.read","tweet.read","user.read"]
# )
# participant_ids: list[str] = SchemaField(
# description="Array of User IDs to create conversation with (max 50)",
# placeholder="Enter participant user IDs",
# default=[],
# advanced=False
# )
# text: str = SchemaField(
# description="Text of the Direct Message (up to 10,000 characters)",
# placeholder="Enter message text",
# default="",
# advanced=False
# )
# media_id: str = SchemaField(
# description="Media ID to attach to the message",
# placeholder="Enter media ID",
# default="",
# advanced=False
# )
# class Output(BlockSchema):
# dm_event_id: str = SchemaField(description="ID of the sent direct message")
# dm_conversation_id: str = SchemaField(description="ID of the conversation")
# error: str = SchemaField(description="Error message if sending failed")
# def __init__(self):
# super().__init__(
# id="ec11cabc-a62e-11ef-8c0e-3fe37ba2ec92",
# description="This block creates a new group DM conversation with specified Twitter users.",
# categories={BlockCategory.SOCIAL},
# input_schema=TwitterCreateDMConversationBlock.Input,
# output_schema=TwitterCreateDMConversationBlock.Output,
# test_input={
# "participant_ids": ["783214", "2244994945"],
# "text": "Hello from Twitter API",
# "media_id": "",
# "credentials": TEST_CREDENTIALS_INPUT
# },
# test_credentials=TEST_CREDENTIALS,
# test_output=[
# ("dm_event_id", "0987654321"),
# ("dm_conversation_id", "1234567890"),
# ("error", "")
# ],
# test_mock={
# "create_dm_conversation": lambda *args, **kwargs: (
# "0987654321",
# "1234567890"
# )
# },
# )
# @staticmethod
# def create_dm_conversation(
# credentials: TwitterCredentials,
# participant_ids: list[str],
# text: str,
# media_id: str
# ):
# try:
# client = tweepy.Client(
# bearer_token=credentials.access_token.get_secret_value()
# )
# response = cast(
# Response,
# client.create_direct_message_conversation(
# participant_ids=participant_ids,
# text=None if text == "" else text,
# media_id=None if media_id == "" else media_id,
# user_auth=False
# )
# )
# if not response.data:
# raise Exception("Failed to create DM conversation")
# return response.data["dm_event_id"], response.data["dm_conversation_id"]
# except tweepy.TweepyException:
# raise
# except Exception as e:
# print(f"Unexpected error: {str(e)}")
# raise
# def run(
# self,
# input_data: Input,
# *,
# credentials: TwitterCredentials,
# **kwargs,
# ) -> BlockOutput:
# try:
# dm_event_id, dm_conversation_id = self.create_dm_conversation(
# credentials,
# input_data.participant_ids,
# input_data.text,
# input_data.media_id
# )
# yield "dm_event_id", dm_event_id
# yield "dm_conversation_id", dm_conversation_id
# except Exception as e:
# yield "error", handle_tweepy_exception(e)

View File

@ -0,0 +1,481 @@
# from typing import cast
import tweepy
# from tweepy.client import Response
from backend.data.block import Block, BlockCategory, BlockOutput, BlockSchema
from backend.data.model import SchemaField
# from backend.blocks.twitter._builders import UserExpansionsBuilder
# from backend.blocks.twitter._types import TweetFields, TweetUserFields, UserExpansionInputs, UserExpansions
from backend.blocks.twitter.tweepy_exceptions import handle_tweepy_exception
from backend.blocks.twitter._auth import (
TEST_CREDENTIALS,
TEST_CREDENTIALS_INPUT,
TwitterCredentials,
TwitterCredentialsField,
TwitterCredentialsInput,
)
class TwitterUnfollowListBlock(Block):
"""
Unfollows a Twitter list for the authenticated user
"""
class Input(BlockSchema):
credentials: TwitterCredentialsInput = TwitterCredentialsField(
["follows.write", "offline.access"]
)
list_id: str = SchemaField(
description="The ID of the List to unfollow",
placeholder="Enter list ID",
)
class Output(BlockSchema):
success: bool = SchemaField(description="Whether the unfollow was successful")
error: str = SchemaField(description="Error message if the request failed")
def __init__(self):
super().__init__(
id="1f43310a-a62f-11ef-8276-2b06a1bbae1a",
description="This block unfollows a specified Twitter list for the authenticated user.",
categories={BlockCategory.SOCIAL},
input_schema=TwitterUnfollowListBlock.Input,
output_schema=TwitterUnfollowListBlock.Output,
test_input={
"list_id": "123456789",
"credentials": TEST_CREDENTIALS_INPUT
},
test_credentials=TEST_CREDENTIALS,
test_output=[
("success", True),
],
)
@staticmethod
def unfollow_list(
credentials: TwitterCredentials,
list_id: str
):
try:
client = tweepy.Client(
bearer_token=credentials.access_token.get_secret_value()
)
client.unfollow_list(list_id=list_id,user_auth=False)
return True
except tweepy.TweepyException:
raise
def run(
self,
input_data: Input,
*,
credentials: TwitterCredentials,
**kwargs,
) -> BlockOutput:
try:
success = self.unfollow_list(
credentials,
input_data.list_id
)
yield "success", success
except Exception as e:
yield "error", handle_tweepy_exception(e)
class TwitterFollowListBlock(Block):
"""
Follows a Twitter list for the authenticated user
"""
class Input(BlockSchema):
credentials: TwitterCredentialsInput = TwitterCredentialsField(
["tweet.read","users.read","list.write", "offline.access"]
)
list_id: str = SchemaField(
description="The ID of the List to follow",
placeholder="Enter list ID",
)
class Output(BlockSchema):
success: bool = SchemaField(description="Whether the follow was successful")
error: str = SchemaField(description="Error message if the request failed")
def __init__(self):
super().__init__(
id="03d8acf6-a62f-11ef-b17f-b72b04a09e79",
description="This block follows a specified Twitter list for the authenticated user.",
categories={BlockCategory.SOCIAL},
input_schema=TwitterFollowListBlock.Input,
output_schema=TwitterFollowListBlock.Output,
test_input={
"list_id": "123456789",
"credentials": TEST_CREDENTIALS_INPUT
},
test_credentials=TEST_CREDENTIALS,
test_output=[
("success", True),
("error", None)
],
)
@staticmethod
def follow_list(
credentials: TwitterCredentials,
list_id: str
):
try:
client = tweepy.Client(
bearer_token=credentials.access_token.get_secret_value()
)
client.follow_list(list_id=list_id,user_auth=False)
return True
except tweepy.TweepyException:
raise
def run(
self,
input_data: Input,
*,
credentials: TwitterCredentials,
**kwargs,
) -> BlockOutput:
try:
success = self.follow_list(
credentials,
input_data.list_id
)
yield "success", success
except Exception as e:
yield "error", handle_tweepy_exception(e)
# Enterprise Level [Need to do Manual testing]
# class TwitterListGetFollowersBlock(Block):
# """
# Gets followers of a specified Twitter list
# """
# class Input(UserExpansionInputs):
# credentials: TwitterCredentialsInput = TwitterCredentialsField(
# ["tweet.read","users.read", "list.read", "offline.access"]
# )
# list_id: str = SchemaField(
# description="The ID of the List to get followers for",
# placeholder="Enter list ID",
# required=True
# )
# max_results: int = SchemaField(
# description="Max number of results per page (1-100)",
# placeholder="Enter max results",
# default=10,
# advanced=True,
# )
# pagination_token: str = SchemaField(
# description="Token for pagination",
# placeholder="Enter pagination token",
# default="",
# advanced=True,
# )
# class Output(BlockSchema):
# user_ids: list[str] = SchemaField(description="List of user IDs of followers")
# usernames: list[str] = SchemaField(description="List of usernames of followers")
# next_token: str = SchemaField(description="Token for next page of results")
# data: list[dict] = SchemaField(description="Complete follower data")
# included: dict = SchemaField(description="Additional data requested via expansions")
# meta: dict = SchemaField(description="Metadata about the response")
# error: str = SchemaField(description="Error message if the request failed")
# def __init__(self):
# super().__init__(
# id="16b289b4-a62f-11ef-95d4-bb29b849eb99",
# description="This block retrieves followers of a specified Twitter list.",
# categories={BlockCategory.SOCIAL},
# input_schema=TwitterListGetFollowersBlock.Input,
# output_schema=TwitterListGetFollowersBlock.Output,
# test_input={
# "list_id": "123456789",
# "max_results": 10,
# "pagination_token": None,
# "credentials": TEST_CREDENTIALS_INPUT,
# "expansions": [],
# "tweet_fields": [],
# "user_fields": []
# },
# test_credentials=TEST_CREDENTIALS,
# test_output=[
# ("user_ids", ["2244994945"]),
# ("usernames", ["testuser"]),
# ("next_token", None),
# ("data", {"followers": [{"id": "2244994945", "username": "testuser"}]}),
# ("included", {}),
# ("meta", {}),
# ("error", "")
# ],
# test_mock={
# "get_list_followers": lambda *args, **kwargs: ({
# "followers": [{"id": "2244994945", "username": "testuser"}]
# }, {}, {}, ["2244994945"], ["testuser"], None)
# }
# )
# @staticmethod
# def get_list_followers(
# credentials: TwitterCredentials,
# list_id: str,
# max_results: int,
# pagination_token: str,
# expansions: list[UserExpansions],
# tweet_fields: list[TweetFields],
# user_fields: list[TweetUserFields]
# ):
# try:
# client = tweepy.Client(
# bearer_token=credentials.access_token.get_secret_value(),
# )
# params = {
# "id": list_id,
# "max_results": max_results,
# "pagination_token": None if pagination_token == "" else pagination_token,
# "user_auth": False
# }
# params = (UserExpansionsBuilder(params)
# .add_expansions(expansions)
# .add_tweet_fields(tweet_fields)
# .add_user_fields(user_fields)
# .build())
# response = cast(
# Response,
# client.get_list_followers(**params)
# )
# meta = {}
# user_ids = []
# usernames = []
# next_token = None
# if response.meta:
# meta = response.meta
# next_token = meta.get("next_token")
# included = IncludesSerializer.serialize(response.includes)
# data = ResponseDataSerializer.serialize_list(response.data)
# if response.data:
# user_ids = [str(item.id) for item in response.data]
# usernames = [item.username for item in response.data]
# return data, included, meta, user_ids, usernames, next_token
# raise Exception("No followers found")
# except tweepy.TweepyException:
# raise
# def run(
# self,
# input_data: Input,
# *,
# credentials: TwitterCredentials,
# **kwargs,
# ) -> BlockOutput:
# try:
# followers_data, included, meta, user_ids, usernames, next_token = self.get_list_followers(
# credentials,
# input_data.list_id,
# input_data.max_results,
# input_data.pagination_token,
# input_data.expansions,
# input_data.tweet_fields,
# input_data.user_fields
# )
# if user_ids:
# yield "user_ids", user_ids
# if usernames:
# yield "usernames", usernames
# if next_token:
# yield "next_token", next_token
# if followers_data:
# yield "data", followers_data
# if included:
# yield "included", included
# if meta:
# yield "meta", meta
# except Exception as e:
# yield "error", handle_tweepy_exception(e)
# class TwitterGetFollowedListsBlock(Block):
# """
# Gets lists followed by a specified Twitter user
# """
# class Input(UserExpansionInputs):
# credentials: TwitterCredentialsInput = TwitterCredentialsField(
# ["follows.read", "users.read", "list.read", "offline.access"]
# )
# user_id: str = SchemaField(
# description="The user ID whose followed Lists to retrieve",
# placeholder="Enter user ID",
# required=True
# )
# max_results: int = SchemaField(
# description="Max number of results per page (1-100)",
# placeholder="Enter max results",
# default=10,
# advanced=True,
# )
# pagination_token: str = SchemaField(
# description="Token for pagination",
# placeholder="Enter pagination token",
# default="",
# advanced=True,
# )
# class Output(BlockSchema):
# list_ids: list[str] = SchemaField(description="List of list IDs")
# list_names: list[str] = SchemaField(description="List of list names")
# data: list[dict] = SchemaField(description="Complete list data")
# includes: dict = SchemaField(description="Additional data requested via expansions")
# meta: dict = SchemaField(description="Metadata about the response")
# next_token: str = SchemaField(description="Token for next page of results")
# error: str = SchemaField(description="Error message if the request failed")
# def __init__(self):
# super().__init__(
# id="0e18bbfc-a62f-11ef-94fa-1f1e174b809e",
# description="This block retrieves all Lists a specified user follows.",
# categories={BlockCategory.SOCIAL},
# input_schema=TwitterGetFollowedListsBlock.Input,
# output_schema=TwitterGetFollowedListsBlock.Output,
# test_input={
# "user_id": "123456789",
# "max_results": 10,
# "pagination_token": None,
# "credentials": TEST_CREDENTIALS_INPUT,
# "expansions": [],
# "tweet_fields": [],
# "user_fields": []
# },
# test_credentials=TEST_CREDENTIALS,
# test_output=[
# ("list_ids", ["12345"]),
# ("list_names", ["Test List"]),
# ("data", {"followed_lists": [{"id": "12345", "name": "Test List"}]}),
# ("includes", {}),
# ("meta", {}),
# ("next_token", None),
# ("error", "")
# ],
# test_mock={
# "get_followed_lists": lambda *args, **kwargs: ({
# "followed_lists": [{"id": "12345", "name": "Test List"}]
# }, {}, {}, ["12345"], ["Test List"], None)
# }
# )
# @staticmethod
# def get_followed_lists(
# credentials: TwitterCredentials,
# user_id: str,
# max_results: int,
# pagination_token: str,
# expansions: list[UserExpansions],
# tweet_fields: list[TweetFields],
# user_fields: list[TweetUserFields]
# ):
# try:
# client = tweepy.Client(
# bearer_token=credentials.access_token.get_secret_value(),
# )
# params = {
# "id": user_id,
# "max_results": max_results,
# "pagination_token": None if pagination_token == "" else pagination_token,
# "user_auth": False
# }
# params = (UserExpansionsBuilder(params)
# .add_expansions(expansions)
# .add_tweet_fields(tweet_fields)
# .add_user_fields(user_fields)
# .build())
# response = cast(
# Response,
# client.get_followed_lists(**params)
# )
# meta = {}
# list_ids = []
# list_names = []
# next_token = None
# if response.meta:
# meta = response.meta
# next_token = meta.get("next_token")
# included = IncludesSerializer.serialize(response.includes)
# data = ResponseDataSerializer.serialize_list(response.data)
# if response.data:
# list_ids = [str(item.id) for item in response.data]
# list_names = [item.name for item in response.data]
# return data, included, meta, list_ids, list_names, next_token
# raise Exception("No followed lists found")
# except tweepy.TweepyException:
# raise
# def run(
# self,
# input_data: Input,
# *,
# credentials: TwitterCredentials,
# **kwargs,
# ) -> BlockOutput:
# try:
# lists_data, included, meta, list_ids, list_names, next_token = self.get_followed_lists(
# credentials,
# input_data.user_id,
# input_data.max_results,
# input_data.pagination_token,
# input_data.expansions,
# input_data.tweet_fields,
# input_data.user_fields
# )
# if list_ids:
# yield "list_ids", list_ids
# if list_names:
# yield "list_names", list_names
# if next_token:
# yield "next_token", next_token
# if lists_data:
# yield "data", lists_data
# if included:
# yield "includes", included
# if meta:
# yield "meta", meta
# except Exception as e:
# yield "error", handle_tweepy_exception(e)

View File

@ -0,0 +1,330 @@
from typing import cast
import tweepy
from tweepy.client import Response
from backend.blocks.twitter._serializer import IncludesSerializer, ResponseDataSerializer
from backend.data.block import Block, BlockCategory, BlockOutput, BlockSchema
from backend.data.model import SchemaField
from backend.blocks.twitter._types import ListExpansionInputs, ListExpansions, ListFields, TweetUserFields
from backend.blocks.twitter.tweepy_exceptions import handle_tweepy_exception
from backend.blocks.twitter._builders import ListExpansionsBuilder
from backend.blocks.twitter._auth import (
TEST_CREDENTIALS,
TEST_CREDENTIALS_INPUT,
TwitterCredentials,
TwitterCredentialsField,
TwitterCredentialsInput,
)
class TwitterGetListBlock(Block):
"""
Gets information about a Twitter List specified by ID
"""
class Input(ListExpansionInputs):
credentials: TwitterCredentialsInput = TwitterCredentialsField(
["tweet.read", "users.read", "offline.access"]
)
list_id: str = SchemaField(
description="The ID of the List to lookup",
placeholder="Enter list ID",
required=True
)
class Output(BlockSchema):
# Common outputs
id: str = SchemaField(description="ID of the Twitter List")
name: str = SchemaField(description="Name of the Twitter List")
owner_id: str = SchemaField(description="ID of the List owner")
owner_username: str = SchemaField(description="Username of the List owner")
# Complete outputs
data: dict = SchemaField(description="Complete list data")
included: dict = SchemaField(description="Additional data requested via expansions")
meta: dict = SchemaField(description="Metadata about the response")
error: str = SchemaField(description="Error message if the request failed")
def __init__(self):
super().__init__(
id="34ebc80a-a62f-11ef-9c2a-3fcab6c07079",
description="This block retrieves information about a specified Twitter List.",
categories={BlockCategory.SOCIAL},
input_schema=TwitterGetListBlock.Input,
output_schema=TwitterGetListBlock.Output,
test_input={
"list_id": "84839422",
"credentials": TEST_CREDENTIALS_INPUT,
"expansions": [],
"list_fields": [],
"user_fields": []
},
test_credentials=TEST_CREDENTIALS,
test_output=[
("id", "84839422"),
("name", "Official Twitter Accounts"),
("owner_id", "2244994945"),
("owner_username", "TwitterAPI"),
("data", {"id": "84839422", "name": "Official Twitter Accounts"}),
("included", {}),
("meta", {}),
("error", "")
],
test_mock={
"get_list": lambda *args, **kwargs: ({
"id": "84839422",
"name": "Official Twitter Accounts"
}, {})
}
)
@staticmethod
def get_list(
credentials: TwitterCredentials,
list_id: str,
expansions: list[ListExpansions],
user_fields: list[TweetUserFields],
list_fields: list[ListFields],
):
try:
client = tweepy.Client(
bearer_token=credentials.access_token.get_secret_value()
)
params = {
"id": list_id,
"user_auth": False
}
params = (ListExpansionsBuilder(params)
.add_expansions(expansions)
.add_user_fields(user_fields)
.add_list_fields(list_fields)
.build())
response = cast(
Response,
client.get_list(
**params
)
)
meta = {}
owner_id = ""
owner_username = ""
included = IncludesSerializer.serialize(response.includes)
data_dict = ResponseDataSerializer.serialize_dict(response.data)
if "users" in included:
owner_id = str(included["users"][0]["id"])
owner_username = included["users"][0]["username"]
if response.meta:
meta = response.meta
if response.data:
return data_dict, included, meta, owner_id, owner_username
raise Exception("List not found")
except tweepy.TweepyException:
raise
def run(
self,
input_data: Input,
*,
credentials: TwitterCredentials,
**kwargs,
) -> BlockOutput:
try:
list_data, included, meta, owner_id, owner_username = self.get_list(
credentials,
input_data.list_id,
input_data.expansions,
input_data.user_fields,
input_data.list_fields
)
yield "id", str(list_data["id"])
yield "name", list_data["name"]
if owner_id:
yield "owner_id", owner_id
if owner_username:
yield "owner_username", owner_username
yield "data", {"id": list_data["id"], "name": list_data["name"]}
if included:
yield "included", included
if meta:
yield "meta", meta
except Exception as e:
yield "error", handle_tweepy_exception(e)
class TwitterGetOwnedListsBlock(Block):
"""
Gets all Lists owned by the specified user
"""
class Input(ListExpansionInputs):
credentials: TwitterCredentialsInput = TwitterCredentialsField(
["tweet.read", "users.read","list.read", "offline.access"]
)
user_id: str = SchemaField(
description="The user ID whose owned Lists to retrieve",
placeholder="Enter user ID",
required=True
)
max_results: int = SchemaField(
description="Maximum number of results per page (1-100)",
placeholder="Enter max results (default 100)",
advanced=True,
default=10,
)
pagination_token: str = SchemaField(
description="Token for pagination",
placeholder="Enter pagination token",
advanced=True,
default=""
)
class Output(BlockSchema):
# Common outputs
list_ids: list[str] = SchemaField(description="List ids of the owned lists")
list_names: list[str] = SchemaField(description="List names of the owned lists")
next_token: str = SchemaField(description="Token for next page of results")
# Complete outputs
data: list[dict] = SchemaField(description="Complete owned lists data")
included: dict = SchemaField(description="Additional data requested via expansions")
meta: dict = SchemaField(description="Metadata about the response")
error: str = SchemaField(description="Error message if the request failed")
def __init__(self):
super().__init__(
id="2b6bdb26-a62f-11ef-a9ce-ff89c2568726",
description="This block retrieves all Lists owned by a specified Twitter user.",
categories={BlockCategory.SOCIAL},
input_schema=TwitterGetOwnedListsBlock.Input,
output_schema=TwitterGetOwnedListsBlock.Output,
test_input={
"user_id": "2244994945",
"max_results": 10,
"credentials": TEST_CREDENTIALS_INPUT,
"expansions": [],
"list_fields": [],
"user_fields": []
},
test_credentials=TEST_CREDENTIALS,
test_output=[
("list_ids", ["84839422"]),
("list_names", ["Official Twitter Accounts"]),
("next_token", None),
("data", {"owned_lists": [{"id": "84839422", "name": "Official Twitter Accounts"}]}),
("included", {}),
("meta", {}),
("error", "")
],
test_mock={
"get_owned_lists": lambda *args, **kwargs: ({
"owned_lists": [{"id": "84839422", "name": "Official Twitter Accounts"}]
}, {}, {}, ["84839422"], ["Official Twitter Accounts"], None)
}
)
@staticmethod
def get_owned_lists(
credentials: TwitterCredentials,
user_id: str,
max_results: int,
pagination_token: str,
expansions: list[ListExpansions],
user_fields: list[TweetUserFields],
list_fields: list[ListFields]
):
try:
client = tweepy.Client(
bearer_token=credentials.access_token.get_secret_value()
)
params = {
"id": user_id,
"max_results": max_results,
"pagination_token": None if pagination_token == "" else pagination_token,
"user_auth": False
}
params = (ListExpansionsBuilder(params)
.add_expansions(expansions)
.add_user_fields(user_fields)
.add_list_fields(list_fields)
.build())
response = cast(
Response,
client.get_owned_lists(**params)
)
meta = {}
list_ids = []
list_names = []
next_token = None
if response.meta:
meta = response.meta
next_token = meta.get("next_token")
included = IncludesSerializer.serialize(response.includes)
data = ResponseDataSerializer.serialize_list(response.data)
if response.data:
list_ids = [str(item.id) for item in response.data]
list_names = [item.name for item in response.data]
return data, included, meta, list_ids, list_names, next_token
raise Exception("Lists not found")
except tweepy.TweepyException:
raise
def run(
self,
input_data: Input,
*,
credentials: TwitterCredentials,
**kwargs,
) -> BlockOutput:
try:
list_data, included, meta, list_ids, list_names, next_token = self.get_owned_lists(
credentials,
input_data.user_id,
input_data.max_results,
input_data.pagination_token,
input_data.expansions,
input_data.user_fields,
input_data.list_fields
)
if list_ids:
yield "list_ids", list_ids
if list_names:
yield "list_names", list_names
if next_token:
yield "next_token", next_token
if list_data:
yield "data", list_data
if included:
yield "included", included
if meta:
yield "meta", meta
except Exception as e:
yield "error", handle_tweepy_exception(e)

View File

@ -0,0 +1,528 @@
from typing import cast
from backend.blocks.twitter._serializer import IncludesSerializer, ResponseDataSerializer
import tweepy
from tweepy.client import Response
from backend.data.block import Block, BlockCategory, BlockOutput, BlockSchema
from backend.data.model import SchemaField
from backend.blocks.twitter._builders import ListExpansionsBuilder, UserExpansionsBuilder
from backend.blocks.twitter._types import ListExpansionInputs, ListExpansions, ListFields, TweetFields, TweetUserFields, UserExpansionInputs, UserExpansions
from backend.blocks.twitter.tweepy_exceptions import handle_tweepy_exception
from backend.blocks.twitter._auth import (
TEST_CREDENTIALS,
TEST_CREDENTIALS_INPUT,
TwitterCredentials,
TwitterCredentialsField,
TwitterCredentialsInput,
)
class TwitterRemoveListMemberBlock(Block):
"""
Removes a member from a Twitter List that the authenticated user owns
"""
class Input(BlockSchema):
credentials: TwitterCredentialsInput = TwitterCredentialsField(
["list.write","users.read","tweet.read", "offline.access"]
)
list_id: str = SchemaField(
description="The ID of the List to remove the member from",
placeholder="Enter list ID",
required=True
)
user_id: str = SchemaField(
description="The ID of the user to remove from the List",
placeholder="Enter user ID to remove",
required=True
)
class Output(BlockSchema):
success: bool = SchemaField(
description="Whether the member was successfully removed"
)
error: str = SchemaField(
description="Error message if the removal failed"
)
def __init__(self):
super().__init__(
id="5a3d1320-a62f-11ef-b7ce-a79e7656bcb0",
description="This block removes a specified user from a Twitter List owned by the authenticated user.",
categories={BlockCategory.SOCIAL},
input_schema=TwitterRemoveListMemberBlock.Input,
output_schema=TwitterRemoveListMemberBlock.Output,
test_input={
"list_id": "123456789",
"user_id": "987654321",
"credentials": TEST_CREDENTIALS_INPUT
},
test_credentials=TEST_CREDENTIALS,
test_output=[("success", True)],
test_mock={
"remove_list_member": lambda *args, **kwargs: True
},
)
@staticmethod
def remove_list_member(
credentials: TwitterCredentials,
list_id: str,
user_id: str
):
try:
client = tweepy.Client(
bearer_token=credentials.access_token.get_secret_value()
)
client.remove_list_member(
id=list_id,
user_id=user_id,
user_auth=False
)
return True
except tweepy.TweepyException:
raise
except Exception as e:
print(f"Unexpected error: {str(e)}")
raise
def run(
self,
input_data: Input,
*,
credentials: TwitterCredentials,
**kwargs,
) -> BlockOutput:
try:
success = self.remove_list_member(
credentials,
input_data.list_id,
input_data.user_id
)
yield "success", success
except Exception as e:
yield "error", handle_tweepy_exception(e)
class TwitterAddListMemberBlock(Block):
"""
Adds a member to a Twitter List that the authenticated user owns
"""
class Input(BlockSchema):
credentials: TwitterCredentialsInput = TwitterCredentialsField(
["list.write", "users.read", "tweet.read", "offline.access"]
)
list_id: str = SchemaField(
description="The ID of the List to add the member to",
placeholder="Enter list ID",
required=True
)
user_id: str = SchemaField(
description="The ID of the user to add to the List",
placeholder="Enter user ID to add",
required=True
)
class Output(BlockSchema):
success: bool = SchemaField(
description="Whether the member was successfully added"
)
error: str = SchemaField(
description="Error message if the addition failed"
)
def __init__(self):
super().__init__(
id="3ee8284e-a62f-11ef-84e4-8f6e2cbf0ddb",
description="This block adds a specified user to a Twitter List owned by the authenticated user.",
categories={BlockCategory.SOCIAL},
input_schema=TwitterAddListMemberBlock.Input,
output_schema=TwitterAddListMemberBlock.Output,
test_input={
"list_id": "123456789",
"user_id": "987654321",
"credentials": TEST_CREDENTIALS_INPUT
},
test_credentials=TEST_CREDENTIALS,
test_output=[("success", True)],
test_mock={
"add_list_member": lambda *args, **kwargs: True
},
)
@staticmethod
def add_list_member(
credentials: TwitterCredentials,
list_id: str,
user_id: str
):
try:
client = tweepy.Client(
bearer_token=credentials.access_token.get_secret_value()
)
client.add_list_member(
id=list_id,
user_id=user_id,
user_auth=False
)
return True
except tweepy.TweepyException:
raise
except Exception as e:
print(f"Unexpected error: {str(e)}")
raise
def run(
self,
input_data: Input,
*,
credentials: TwitterCredentials,
**kwargs,
) -> BlockOutput:
try:
success = self.add_list_member(
credentials,
input_data.list_id,
input_data.user_id
)
yield "success", success
except Exception as e:
yield "error", handle_tweepy_exception(e)
class TwitterGetListMembersBlock(Block):
"""
Gets the members of a specified Twitter List
"""
class Input(UserExpansionInputs):
credentials: TwitterCredentialsInput = TwitterCredentialsField(
["list.read", "offline.access"]
)
list_id: str = SchemaField(
description="The ID of the List to get members from",
placeholder="Enter list ID",
required=True
)
max_results: int = SchemaField(
description="Maximum number of results per page (1-100)",
placeholder="Enter max results",
default=10,
advanced=True
)
pagination_token: str = SchemaField(
description="Token for pagination of results",
placeholder="Enter pagination token",
default="",
advanced=True
)
class Output(BlockSchema):
ids: list[str] = SchemaField(description="List of member user IDs")
usernames: list[str] = SchemaField(description="List of member usernames")
next_token: str = SchemaField(description="Next token for pagination")
data: list[dict] = SchemaField(description="Complete user data for list members")
included: dict = SchemaField(description="Additional data requested via expansions")
meta: dict = SchemaField(description="Metadata including pagination info")
error: str = SchemaField(description="Error message if the request failed")
def __init__(self):
super().__init__(
id="4dba046e-a62f-11ef-b69a-87240c84b4c7",
description="This block retrieves the members of a specified Twitter List.",
categories={BlockCategory.SOCIAL},
input_schema=TwitterGetListMembersBlock.Input,
output_schema=TwitterGetListMembersBlock.Output,
test_input={
"list_id": "123456789",
"max_results": 10,
"pagination_token": "",
"credentials": TEST_CREDENTIALS_INPUT,
"expansions": [],
"tweet_fields": [],
"user_fields": []
},
test_credentials=TEST_CREDENTIALS,
test_output=[
("ids", ["12345", "67890"]),
("usernames", ["testuser1", "testuser2"]),
("data", [
{"id": "12345", "username": "testuser1"},
{"id": "67890", "username": "testuser2"}
]),
("included", {}),
("meta", {"next_token": "next_token_value"}),
("next_token", "next_token_value")
],
test_mock={
"get_list_members": lambda *args, **kwargs: (
["12345", "67890"],
["testuser1", "testuser2"],
[{"id": "12345", "username": "testuser1"}, {"id": "67890", "username": "testuser2"}],
{},
{"next_token": "next_token_value"},
"next_token_value"
)
}
)
@staticmethod
def get_list_members(
credentials: TwitterCredentials,
list_id: str,
max_results: int,
pagination_token: str,
expansions: list[UserExpansions],
tweet_fields: list[TweetFields],
user_fields: list[TweetUserFields],
):
try:
client = tweepy.Client(
bearer_token=credentials.access_token.get_secret_value()
)
params = {
"id": list_id,
"max_results": max_results,
"pagination_token": None if pagination_token == "" else pagination_token,
"user_auth": False
}
params = (UserExpansionsBuilder(params)
.add_expansions(expansions)
.add_tweet_fields(tweet_fields)
.add_user_fields(user_fields)
.build())
response = cast(
Response,
client.get_list_members(**params)
)
meta = {}
next_token = None
user_ids = []
usernames = []
if response.meta:
meta = response.meta
next_token = meta.get("next_token")
included = IncludesSerializer.serialize(response.includes)
data = ResponseDataSerializer.serialize_list(response.data)
if response.data:
user_ids = [str(user.id) for user in response.data]
usernames = [user.username for user in response.data]
return user_ids, usernames, data, included, meta, next_token
raise Exception("List members not found")
except tweepy.TweepyException:
raise
def run(
self,
input_data: Input,
*,
credentials: TwitterCredentials,
**kwargs,
) -> BlockOutput:
try:
ids, usernames, data, included, meta, next_token = self.get_list_members(
credentials,
input_data.list_id,
input_data.max_results,
input_data.pagination_token,
input_data.expansions,
input_data.tweet_fields,
input_data.user_fields
)
if ids:
yield "ids", ids
if usernames:
yield "usernames", usernames
if next_token:
yield "next_token", next_token
if data:
yield "data", data
if included:
yield "included", included
if meta:
yield "meta", meta
except Exception as e:
yield "error", handle_tweepy_exception(e)
class TwitterGetListMembershipsBlock(Block):
"""
Gets all Lists that a specified user is a member of
"""
class Input(ListExpansionInputs):
credentials: TwitterCredentialsInput = TwitterCredentialsField(
["list.read", "offline.access"]
)
user_id: str = SchemaField(
description="The ID of the user whose List memberships to retrieve",
placeholder="Enter user ID",
required=True
)
max_results: int = SchemaField(
description="Maximum number of results per page (1-100)",
placeholder="Enter max results",
advanced=True,
default=10,
)
pagination_token: str = SchemaField(
description="Token for pagination of results",
placeholder="Enter pagination token",
advanced=True,
default=""
)
class Output(BlockSchema):
list_ids: list[str] = SchemaField(description="List of list IDs")
next_token: str = SchemaField(description="Next token for pagination")
data: list[dict] = SchemaField(description="List membership data")
included: dict = SchemaField(description="Additional data requested via expansions")
meta: dict = SchemaField(description="Metadata about pagination")
error: str = SchemaField(description="Error message if the request failed")
def __init__(self):
super().__init__(
id="46e6429c-a62f-11ef-81c0-2b55bc7823ba",
description="This block retrieves all Lists that a specified user is a member of.",
categories={BlockCategory.SOCIAL},
input_schema=TwitterGetListMembershipsBlock.Input,
output_schema=TwitterGetListMembershipsBlock.Output,
test_input={
"user_id": "123456789",
"max_results": 50,
"pagination_token": None,
"credentials": TEST_CREDENTIALS_INPUT,
"expansions": [],
"list_fields": [],
"user_fields": []
},
test_credentials=TEST_CREDENTIALS,
test_output=[
("list_ids", ["84839422"]),
("data", {"lists": [{"id": "84839422"}]}),
("included", {}),
("meta", {"next_token": None}),
("next_token", None)
],
test_mock={
"get_list_memberships": lambda *args, **kwargs: (
{"lists": [{"id": "84839422"}]},
{},
{"next_token": None},
["84839422"],
None
)
}
)
@staticmethod
def get_list_memberships(
credentials: TwitterCredentials,
user_id: str,
max_results: int,
pagination_token: str,
expansions: list[ListExpansions],
user_fields: list[TweetUserFields],
list_fields: list[ListFields]
):
try:
client = tweepy.Client(
bearer_token=credentials.access_token.get_secret_value()
)
params = {
"id": user_id,
"max_results": max_results,
"pagination_token": None if pagination_token == "" else pagination_token,
"user_auth": False
}
params = (ListExpansionsBuilder(params)
.add_expansions(expansions)
.add_user_fields(user_fields)
.add_list_fields(list_fields)
.build())
response = cast(
Response,
client.get_list_memberships(**params)
)
meta = {}
next_token = None
list_ids = []
if response.meta:
meta = response.meta
next_token = meta.get("next_token")
included = IncludesSerializer.serialize(response.includes)
data = ResponseDataSerializer.serialize_list(response.data)
if response.data:
list_ids = [str(lst.id) for lst in response.data]
return data, included, meta, list_ids, next_token
raise Exception("List memberships not found")
except tweepy.TweepyException:
raise
except Exception:
raise
def run(
self,
input_data: Input,
*,
credentials: TwitterCredentials,
**kwargs,
) -> BlockOutput:
try:
data, included, meta, list_ids, next_token = self.get_list_memberships(
credentials,
input_data.user_id,
input_data.max_results,
input_data.pagination_token,
input_data.expansions,
input_data.user_fields,
input_data.list_fields
)
if list_ids:
yield "list_ids", list_ids
if next_token:
yield "next_token", next_token
if data:
yield "data", data
if included:
yield "included", included
if meta:
yield "meta", meta
except Exception as e:
yield "error", handle_tweepy_exception(e)

View File

@ -0,0 +1,195 @@
from typing import cast
from backend.blocks.twitter._serializer import IncludesSerializer, ResponseDataSerializer
import tweepy
from tweepy.client import Response
from backend.blocks.twitter._builders import TweetExpansionsBuilder
from backend.blocks.twitter._types import TweetExpansionInputs, TweetExpansions, TweetFields, TweetMediaFields, TweetPlaceFields, TweetPollFields, TweetUserFields
from backend.blocks.twitter.tweepy_exceptions import handle_tweepy_exception
from backend.data.block import Block, BlockCategory, BlockOutput, BlockSchema
from backend.data.model import SchemaField
from backend.blocks.twitter._auth import (
TEST_CREDENTIALS,
TEST_CREDENTIALS_INPUT,
TwitterCredentials,
TwitterCredentialsField,
TwitterCredentialsInput,
)
class TwitterGetListTweetsBlock(Block):
"""
Gets tweets from a specified Twitter list
"""
class Input(TweetExpansionInputs):
credentials: TwitterCredentialsInput = TwitterCredentialsField(
["tweet.read", "offline.access"]
)
list_id: str = SchemaField(
description="The ID of the List whose Tweets you would like to retrieve",
placeholder="Enter list ID",
required=True
)
max_results: int = SchemaField(
description="Maximum number of results per page (1-100)",
placeholder="Enter max results",
default=10,
advanced=True,
)
pagination_token: str = SchemaField(
description="Token for paginating through results",
placeholder="Enter pagination token",
default = "",
advanced=True,
)
class Output(BlockSchema):
# Common outputs
tweet_ids: list[str] = SchemaField(description="List of tweet IDs")
texts: list[str] = SchemaField(description="List of tweet texts")
next_token: str = SchemaField(description="Token for next page of results")
# Complete outputs
data: list[dict] = SchemaField(description="Complete list tweets data")
included: dict = SchemaField(description="Additional data requested via expansions")
meta: dict = SchemaField(description="Response metadata including pagination tokens")
error: str = SchemaField(description="Error message if the request failed")
def __init__(self):
super().__init__(
id="6657edb0-a62f-11ef-8c10-0326d832467d",
description="This block retrieves tweets from a specified Twitter list.",
categories={BlockCategory.SOCIAL},
input_schema=TwitterGetListTweetsBlock.Input,
output_schema=TwitterGetListTweetsBlock.Output,
test_input={
"list_id": "84839422",
"max_results": 10,
"pagination_token": "",
"credentials": TEST_CREDENTIALS_INPUT,
"expansions": [],
"media_fields": [],
"place_fields": [],
"poll_fields": [],
"tweet_fields": [],
"user_fields": []
},
test_credentials=TEST_CREDENTIALS,
test_output=[
("tweet_ids", ["1234567890"]),
("texts", ["Test tweet"]),
("next_token", None),
("data", {"list_tweets": [{"id": "1234567890", "text": "Test tweet"}]}),
("included", {}),
("meta", {}),
("error", "")
],
test_mock={
"get_list_tweets": lambda *args, **kwargs: ({
"list_tweets": [{"id": "1234567890", "text": "Test tweet"}]
}, {}, {}, ["1234567890"], ["Test tweet"], None)
}
)
@staticmethod
def get_list_tweets(
credentials: TwitterCredentials,
list_id: str,
max_results: int,
pagination_token: str,
expansions: list[TweetExpansions],
media_fields: list[TweetMediaFields],
place_fields: list[TweetPlaceFields],
poll_fields: list[TweetPollFields],
tweet_fields: list[TweetFields],
user_fields: list[TweetUserFields]
):
try:
client = tweepy.Client(
bearer_token=credentials.access_token.get_secret_value()
)
params = {
"id": list_id,
"max_results": max_results,
"pagination_token": None if pagination_token == "" else pagination_token,
"user_auth": False
}
params = (TweetExpansionsBuilder(params)
.add_expansions(expansions)
.add_media_fields(media_fields)
.add_place_fields(place_fields)
.add_poll_fields(poll_fields)
.add_tweet_fields(tweet_fields)
.add_user_fields(user_fields)
.build())
response = cast(
Response,
client.get_list_tweets(**params)
)
meta = {}
tweet_ids = []
texts = []
next_token = None
if response.meta:
meta = response.meta
next_token = meta.get("next_token")
included = IncludesSerializer.serialize(response.includes)
data = ResponseDataSerializer.serialize_list(response.data)
if response.data:
tweet_ids = [str(item.id) for item in response.data]
texts = [item.text for item in response.data]
return data, included, meta, tweet_ids, texts, next_token
raise Exception("No tweets found in this list")
except tweepy.TweepyException:
raise
def run(
self,
input_data: Input,
*,
credentials: TwitterCredentials,
**kwargs,
) -> BlockOutput:
try:
list_data, included, meta, tweet_ids, texts, next_token = self.get_list_tweets(
credentials,
input_data.list_id,
input_data.max_results,
input_data.pagination_token,
input_data.expansions,
input_data.media_fields,
input_data.place_fields,
input_data.poll_fields,
input_data.tweet_fields,
input_data.user_fields
)
if tweet_ids:
yield "tweet_ids", tweet_ids
if texts:
yield "texts", texts
if next_token:
yield "next_token", next_token
if list_data:
yield "data", list_data
if included:
yield "included", included
if meta:
yield "meta", meta
except Exception as e:
yield "error", handle_tweepy_exception(e)

View File

@ -0,0 +1,296 @@
from typing import cast
import tweepy
from tweepy.client import Response
from backend.data.block import Block, BlockCategory, BlockOutput, BlockSchema
from backend.data.model import SchemaField
from backend.blocks.twitter.tweepy_exceptions import handle_tweepy_exception
from backend.blocks.twitter._auth import (
TEST_CREDENTIALS,
TEST_CREDENTIALS_INPUT,
TwitterCredentials,
TwitterCredentialsField,
TwitterCredentialsInput,
)
class TwitterDeleteListBlock(Block):
"""
Deletes a Twitter List owned by the authenticated user
"""
class Input(BlockSchema):
credentials: TwitterCredentialsInput = TwitterCredentialsField(
["list.write", "offline.access"]
)
list_id: str = SchemaField(
description="The ID of the List to be deleted",
placeholder="Enter list ID",
required=True
)
class Output(BlockSchema):
success: bool = SchemaField(description="Whether the deletion was successful")
error: str = SchemaField(description="Error message if the request failed")
def __init__(self):
super().__init__(
id="843c6892-a62f-11ef-a5c8-b71239a78d3b",
description="This block deletes a specified Twitter List owned by the authenticated user.",
categories={BlockCategory.SOCIAL},
input_schema=TwitterDeleteListBlock.Input,
output_schema=TwitterDeleteListBlock.Output,
test_input={
"list_id": "1234567890",
"credentials": TEST_CREDENTIALS_INPUT
},
test_credentials=TEST_CREDENTIALS,
test_output=[("success", True)],
test_mock={
"delete_list": lambda *args, **kwargs: True
},
)
@staticmethod
def delete_list(
credentials: TwitterCredentials,
list_id: str
):
try:
client = tweepy.Client(
bearer_token=credentials.access_token.get_secret_value()
)
client.delete_list(
id=list_id,
user_auth=False
)
return True
except tweepy.TweepyException:
raise
except Exception as e:
print(f"Unexpected error: {str(e)}")
raise
def run(
self,
input_data: Input,
*,
credentials: TwitterCredentials,
**kwargs,
) -> BlockOutput:
try:
success = self.delete_list(
credentials,
input_data.list_id
)
yield "success", success
except Exception as e:
yield "error", handle_tweepy_exception(e)
class TwitterUpdateListBlock(Block):
"""
Updates a Twitter List owned by the authenticated user
"""
class Input(BlockSchema):
credentials: TwitterCredentialsInput = TwitterCredentialsField(
["list.write", "offline.access"]
)
list_id: str = SchemaField(
description="The ID of the List to be updated",
placeholder="Enter list ID",
advanced=False,
)
name: str = SchemaField(
description="New name for the List",
placeholder="Enter list name",
default="",
advanced=False,
)
description: str = SchemaField(
description="New description for the List",
placeholder="Enter list description",
default="",
advanced=False,
)
class Output(BlockSchema):
success: bool = SchemaField(description="Whether the update was successful")
error: str = SchemaField(description="Error message if the request failed")
def __init__(self):
super().__init__(
id="7d12630a-a62f-11ef-90c9-8f5a996612c3",
description="This block updates a specified Twitter List owned by the authenticated user.",
categories={BlockCategory.SOCIAL},
input_schema=TwitterUpdateListBlock.Input,
output_schema=TwitterUpdateListBlock.Output,
test_input={
"list_id": "1234567890",
"name": "Updated List Name",
"description": "Updated List Description",
"private": True,
"credentials": TEST_CREDENTIALS_INPUT
},
test_credentials=TEST_CREDENTIALS,
test_output=[("success", True)],
test_mock={
"update_list": lambda *args, **kwargs: True
},
)
@staticmethod
def update_list(
credentials: TwitterCredentials,
list_id: str,
name: str ,
description: str
):
try:
client = tweepy.Client(
bearer_token=credentials.access_token.get_secret_value()
)
client.update_list(
id=list_id,
name=None if name == "" else name,
description=None if description == "" else description,
user_auth=False
)
return True
except tweepy.TweepyException:
raise
except Exception as e:
print(f"Unexpected error: {str(e)}")
raise
def run(
self,
input_data: Input,
*,
credentials: TwitterCredentials,
**kwargs,
) -> BlockOutput:
try:
success = self.update_list(
credentials,
input_data.list_id,
input_data.name,
input_data.description,
)
yield "success", success
except Exception as e:
yield "error", handle_tweepy_exception(e)
class TwitterCreateListBlock(Block):
"""
Creates a Twitter List owned by the authenticated user
"""
class Input(BlockSchema):
credentials: TwitterCredentialsInput = TwitterCredentialsField(
["list.write", "offline.access"]
)
name: str = SchemaField(
description="The name of the List to be created",
placeholder="Enter list name",
advanced=False,
default="",
)
description: str = SchemaField(
description="Description of the List",
placeholder="Enter list description",
advanced=False,
default=""
)
private: bool = SchemaField(
description="Whether the List should be private",
advanced=False,
default=False
)
class Output(BlockSchema):
url: str = SchemaField(description="URL of the created list")
list_id: str = SchemaField(description="ID of the created list")
error: str = SchemaField(description="Error message if the request failed")
def __init__(self):
super().__init__(
id="724148ba-a62f-11ef-89ba-5349b813ef5f",
description="This block creates a new Twitter List for the authenticated user.",
categories={BlockCategory.SOCIAL},
input_schema=TwitterCreateListBlock.Input,
output_schema=TwitterCreateListBlock.Output,
test_input={
"name": "New List Name",
"description": "New List Description",
"private": True,
"credentials": TEST_CREDENTIALS_INPUT
},
test_credentials=TEST_CREDENTIALS,
test_output=[("list_id", "1234567890"), ("url", "https://twitter.com/i/lists/1234567890")],
test_mock={
"create_list": lambda *args, **kwargs: cast(Response, {"data": {"id": "1234567890"}})
},
)
@staticmethod
def create_list(
credentials: TwitterCredentials,
name: str,
description: str,
private: bool
):
try:
client = tweepy.Client(
bearer_token=credentials.access_token.get_secret_value()
)
response = cast(Response, client.create_list(
name=None if name == "" else name,
description=None if description == "" else description,
private=private,
user_auth=False
))
return response
except tweepy.TweepyException:
raise
except Exception as e:
print(f"Unexpected error: {str(e)}")
raise
def run(
self,
input_data: Input,
*,
credentials: TwitterCredentials,
**kwargs,
) -> BlockOutput:
try:
response = self.create_list(
credentials,
input_data.name,
input_data.description,
input_data.private
)
list_id = str(response.data["id"])
yield "list_id", list_id
yield "url", f"https://twitter.com/i/lists/{list_id}"
except Exception as e:
yield "error", handle_tweepy_exception(e)

View File

@ -0,0 +1,302 @@
from typing import cast
from backend.blocks.twitter._serializer import IncludesSerializer, ResponseDataSerializer
import tweepy
from tweepy.client import Response
from backend.data.block import Block, BlockCategory, BlockOutput, BlockSchema
from backend.data.model import SchemaField
from backend.blocks.twitter._builders import ListExpansionsBuilder
from backend.blocks.twitter._types import ListExpansionInputs, ListExpansions, ListFields, TweetUserFields
from backend.blocks.twitter.tweepy_exceptions import handle_tweepy_exception
from backend.blocks.twitter._auth import (
TEST_CREDENTIALS,
TEST_CREDENTIALS_INPUT,
TwitterCredentials,
TwitterCredentialsField,
TwitterCredentialsInput,
)
class TwitterUnpinListBlock(Block):
"""
Enables the authenticated user to unpin a List.
"""
class Input(BlockSchema):
credentials: TwitterCredentialsInput = TwitterCredentialsField(
["list.write", "users.read","tweet.read", "offline.access"]
)
list_id: str = SchemaField(
description="The ID of the List to unpin",
placeholder="Enter list ID",
required=True
)
class Output(BlockSchema):
success: bool = SchemaField(description="Whether the unpin was successful")
error: str = SchemaField(description="Error message if the request failed")
def __init__(self):
super().__init__(
id="a099c034-a62f-11ef-9622-47d0ceb73555",
description="This block allows the authenticated user to unpin a specified List.",
categories={BlockCategory.SOCIAL},
input_schema=TwitterUnpinListBlock.Input,
output_schema=TwitterUnpinListBlock.Output,
test_input={
"list_id": "123456789",
"credentials": TEST_CREDENTIALS_INPUT
},
test_credentials=TEST_CREDENTIALS,
test_output=[("success", True)],
test_mock={
"unpin_list": lambda *args, **kwargs: True
},
)
@staticmethod
def unpin_list(
credentials: TwitterCredentials,
list_id: str
):
try:
client = tweepy.Client(
bearer_token=credentials.access_token.get_secret_value()
)
client.unpin_list(
list_id=list_id,
user_auth=False
)
return True
except tweepy.TweepyException:
raise
except Exception as e:
print(f"Unexpected error: {str(e)}")
raise
def run(
self,
input_data: Input,
*,
credentials: TwitterCredentials,
**kwargs,
) -> BlockOutput:
try:
success = self.unpin_list(
credentials,
input_data.list_id
)
yield "success", success
except Exception as e:
yield "error", handle_tweepy_exception(e)
class TwitterPinListBlock(Block):
"""
Enables the authenticated user to pin a List.
"""
class Input(BlockSchema):
credentials: TwitterCredentialsInput = TwitterCredentialsField(
["list.write", "users.read","tweet.read", "offline.access"]
)
list_id: str = SchemaField(
description="The ID of the List to pin",
placeholder="Enter list ID",
required=True
)
class Output(BlockSchema):
success: bool = SchemaField(description="Whether the pin was successful")
error: str = SchemaField(description="Error message if the request failed")
def __init__(self):
super().__init__(
id="8ec16e48-a62f-11ef-9f35-f3d6de43a802",
description="This block allows the authenticated user to pin a specified List.",
categories={BlockCategory.SOCIAL},
input_schema=TwitterPinListBlock.Input,
output_schema=TwitterPinListBlock.Output,
test_input={
"list_id": "123456789",
"credentials": TEST_CREDENTIALS_INPUT
},
test_credentials=TEST_CREDENTIALS,
test_output=[("success", True)],
test_mock={
"pin_list": lambda *args, **kwargs: True
},
)
@staticmethod
def pin_list(
credentials: TwitterCredentials,
list_id: str
):
try:
client = tweepy.Client(
bearer_token=credentials.access_token.get_secret_value()
)
client.pin_list(
list_id=list_id,
user_auth=False
)
return True
except tweepy.TweepyException:
raise
except Exception as e:
print(f"Unexpected error: {str(e)}")
raise
def run(
self,
input_data: Input,
*,
credentials: TwitterCredentials,
**kwargs,
) -> BlockOutput:
try:
success = self.pin_list(
credentials,
input_data.list_id
)
yield "success", success
except Exception as e:
yield "error", handle_tweepy_exception(e)
class TwitterGetPinnedListsBlock(Block):
"""
Returns the Lists pinned by the authenticated user.
"""
class Input(ListExpansionInputs):
credentials: TwitterCredentialsInput = TwitterCredentialsField(
["lists.read", "users.read", "offline.access"]
)
class Output(BlockSchema):
list_ids : list[str] = SchemaField(description="List IDs of the pinned lists")
list_names : list[str] = SchemaField(description="List names of the pinned lists")
data: list[dict] = SchemaField(description="Response data containing pinned lists")
included: dict = SchemaField(description="Additional data requested via expansions")
meta: dict = SchemaField(description="Metadata about the response")
error: str = SchemaField(description="Error message if the request failed")
def __init__(self):
super().__init__(
id="97e03aae-a62f-11ef-bc53-5b89cb02888f",
description="This block returns the Lists pinned by the authenticated user.",
categories={BlockCategory.SOCIAL},
input_schema=TwitterGetPinnedListsBlock.Input,
output_schema=TwitterGetPinnedListsBlock.Output,
test_input={
"expansions": [],
"list_fields": [],
"user_fields": [],
"credentials": TEST_CREDENTIALS_INPUT
},
test_credentials=TEST_CREDENTIALS,
test_output=[
("list_ids", ["84839422"]),
("list_names", ["Twitter List"]),
("data", {"pinned_lists": [{"id": "84839422", "name": "Twitter List"}]}),
("included", {}),
("meta", {})
],
test_mock={
"get_pinned_lists": lambda *args, **kwargs: (
{"pinned_lists": [{"id": "84839422", "name": "Twitter List"}]},
{},
{},
["84839422"],
["Twitter List"]
)
},
)
@staticmethod
def get_pinned_lists(
credentials: TwitterCredentials,
expansions: list[ListExpansions],
user_fields: list[TweetUserFields],
list_fields: list[ListFields]
):
try:
client = tweepy.Client(
bearer_token=credentials.access_token.get_secret_value()
)
params = {
"user_auth": False
}
params = (ListExpansionsBuilder(params)
.add_expansions(expansions)
.add_user_fields(user_fields)
.add_list_fields(list_fields)
.build())
response = cast(
Response,
client.get_pinned_lists(**params)
)
meta = {}
list_ids = []
list_names = []
if response.meta:
meta = response.meta
included = IncludesSerializer.serialize(response.includes)
data = ResponseDataSerializer.serialize_list(response.data)
if response.data:
list_ids = [str(item.id) for item in response.data]
list_names = [item.name for item in response.data]
return data, included, meta, list_ids, list_names
raise Exception("Lists not found")
except tweepy.TweepyException:
raise
def run(
self,
input_data: Input,
*,
credentials: TwitterCredentials,
**kwargs,
) -> BlockOutput:
try:
list_data, included, meta, list_ids, list_names = self.get_pinned_lists(
credentials,
input_data.expansions,
input_data.user_fields,
input_data.list_fields
)
if list_ids:
yield "list_ids", list_ids
if list_names:
yield "list_names", list_names
if list_data:
yield "data", list_data
if included:
yield "included", included
if meta:
yield "meta", meta
except Exception as e:
yield "error", handle_tweepy_exception(e)

View File

@ -0,0 +1,189 @@
from typing import cast
from backend.blocks.twitter._serializer import IncludesSerializer, ResponseDataSerializer
import tweepy
from tweepy.client import Response
from backend.data.block import Block, BlockCategory, BlockOutput, BlockSchema
from backend.data.model import SchemaField
from backend.blocks.twitter._builders import SpaceExpansionsBuilder
from backend.blocks.twitter.tweepy_exceptions import handle_tweepy_exception
from backend.blocks.twitter._types import TweetUserFields, SpaceExpansionInputs, SpaceExpansions, SpaceFields, SpaceStates
from backend.blocks.twitter._auth import (
TEST_CREDENTIALS,
TEST_CREDENTIALS_INPUT,
TwitterCredentials,
TwitterCredentialsField,
TwitterCredentialsInput,
)
class TwitterSearchSpacesBlock(Block):
"""
Returns live or scheduled Spaces matching specified search terms [for a week only]
"""
class Input(SpaceExpansionInputs):
credentials: TwitterCredentialsInput = TwitterCredentialsField(
["spaces.read","users.read","tweet.read", "offline.access"]
)
query: str = SchemaField(
description="Search term to find in Space titles",
placeholder="Enter search query",
)
max_results: int = SchemaField(
description="Maximum number of results to return (1-100)",
placeholder="Enter max results",
default=10,
advanced=True
)
state: SpaceStates = SchemaField(
description="Type of Spaces to return (live, scheduled, or all)",
placeholder="Enter state filter",
default=SpaceStates.ALL,
)
class Output(BlockSchema):
# Common outputs that user commonly uses
ids: list[str] = SchemaField(description="List of space IDs")
titles: list[str] = SchemaField(description="List of space titles")
host_ids: list = SchemaField(description="List of host IDs")
next_token: str = SchemaField(description="Next token for pagination")
# Complete outputs for advanced use
data: dict = SchemaField(description="Complete space data")
includes: dict = SchemaField(description="Additional data requested via expansions")
meta: dict = SchemaField(description="Metadata including pagination info")
error: str = SchemaField(description="Error message if the request failed")
def __init__(self):
super().__init__(
id="aaefdd48-a62f-11ef-a73c-3f44df63e276",
description="This block searches for Twitter Spaces based on specified terms.",
categories={BlockCategory.SOCIAL},
input_schema=TwitterSearchSpacesBlock.Input,
output_schema=TwitterSearchSpacesBlock.Output,
test_input={
"query": "tech",
"max_results": 10,
"state": "live",
"credentials": TEST_CREDENTIALS_INPUT,
"pagination": "",
"expansions": [],
"space_fields": [],
"user_fields": []
},
test_credentials=TEST_CREDENTIALS,
test_output=[
("ids", ["1234"]),
("titles", ["Tech Talk"]),
("host_ids", ["5678"]),
("next_token", "next_token_value"),
("data", {"spaces": [{"id": "1234", "title": "Tech Talk", "host_id": "5678"}]}),
("includes", {}),
("meta", {"next_token": "next_token_value"}),
("error", "")
],
test_mock={
"search_spaces": lambda *args, **kwargs: (
{"spaces": [{"id": "1234", "title": "Tech Talk", "host_id": "5678"}]},
{},
{"next_token": "next_token_value"},
"next_token_value",
""
)
}
)
@staticmethod
def search_spaces(
credentials: TwitterCredentials,
query: str,
max_results: int,
state: SpaceStates,
expansions: list[SpaceExpansions],
space_fields: list[SpaceFields],
user_fields: list[TweetUserFields]
):
try:
client = tweepy.Client(
bearer_token=credentials.access_token.get_secret_value()
)
params = {
"query": query,
"max_results": max_results,
"state": state.value
}
params = (SpaceExpansionsBuilder(params)
.add_expansions(expansions)
.add_space_fields(space_fields)
.add_user_fields(user_fields)
.build())
response = cast(
Response,
client.search_spaces(**params)
)
meta = {}
next_token = ""
if response.meta:
meta = response.meta
if "next_token" in meta:
next_token = meta["next_token"]
included = IncludesSerializer.serialize(response.includes)
data = ResponseDataSerializer.serialize_list(response.data)
if response.data:
ids = [str(space["id"]) for space in response.data]
titles = [space["title"] for space in data]
host_ids = [space["host_ids"] for space in data]
return data, included, meta, ids, titles, host_ids, next_token
raise Exception("Spaces not found")
except tweepy.TweepyException:
raise
def run(
self,
input_data: Input,
*,
credentials: TwitterCredentials,
**kwargs,
) -> BlockOutput:
try:
data, included, meta, ids, titles, host_ids, next_token = self.search_spaces(
credentials,
input_data.query,
input_data.max_results,
input_data.state,
input_data.expansions,
input_data.space_fields,
input_data.user_fields
)
if ids:
yield "ids", ids
if titles:
yield "titles", titles
if host_ids:
yield "host_ids", host_ids
if next_token:
yield "next_token", next_token
if data:
yield "data", data
if included:
yield "includes", included
if meta:
yield "meta", meta
except Exception as e:
yield "error", handle_tweepy_exception(e)

View File

@ -0,0 +1,556 @@
from typing import cast
from backend.blocks.twitter._serializer import IncludesSerializer, ResponseDataSerializer
import tweepy
from tweepy.client import Response
from backend.data.block import Block, BlockCategory, BlockOutput, BlockSchema
from backend.data.model import SchemaField
from backend.blocks.twitter._builders import SpaceExpansionsBuilder, TweetExpansionsBuilder, UserExpansionsBuilder
from backend.blocks.twitter._types import SpaceExpansionInputs, SpaceExpansions, SpaceFields, TweetExpansionInputs, TweetExpansions, TweetFields, TweetMediaFields, TweetPlaceFields, TweetPollFields, TweetUserFields, UserExpansionInputs, UserExpansions
from backend.blocks.twitter.tweepy_exceptions import handle_tweepy_exception
from backend.blocks.twitter._auth import (
TEST_CREDENTIALS,
TEST_CREDENTIALS_INPUT,
TwitterCredentials,
TwitterCredentialsField,
TwitterCredentialsInput,
)
class TwitterGetSpacesBlock(Block):
"""
Gets information about multiple Twitter Spaces specified by Space IDs or creator user IDs
"""
class Input(SpaceExpansionInputs):
credentials: TwitterCredentialsInput = TwitterCredentialsField(
["spaces.read", "users.read", "offline.access"]
)
space_ids: list[str] = SchemaField(
description="List of Space IDs to lookup (up to 100)",
placeholder="Enter Space IDs",
default=[],
advanced=False
)
user_ids: list[str] = SchemaField(
description="List of user IDs to lookup their Spaces (up to 100)",
placeholder="Enter user IDs",
default=[],
advanced=False
)
class Output(BlockSchema):
# Common outputs
ids: list[str] = SchemaField(description="List of space IDs")
titles: list[str] = SchemaField(description="List of space titles")
# Complete outputs for advanced use
data: list[dict] = SchemaField(description="Complete space data")
includes: dict = SchemaField(description="Additional data requested via expansions")
error: str = SchemaField(description="Error message if the request failed")
def __init__(self):
super().__init__(
id="d75bd7d8-a62f-11ef-b0d8-c7a9496f617f",
description="This block retrieves information about multiple Twitter Spaces.",
categories={BlockCategory.SOCIAL},
input_schema=TwitterGetSpacesBlock.Input,
output_schema=TwitterGetSpacesBlock.Output,
test_input={
"space_ids": ["1DXxyRYNejbKM"],
"user_ids": None,
"credentials": TEST_CREDENTIALS_INPUT,
"expansions": [],
"space_fields": [],
"user_fields": []
},
test_credentials=TEST_CREDENTIALS,
test_output=[
("ids", ["1DXxyRYNejbKM"]),
("titles", ["Test Space"]),
("host_ids", ["1234567"]),
("data", {"spaces": [{"id": "1DXxyRYNejbKM", "title": "Test Space", "host_id": "1234567"}]}),
("includes", {})
],
)
@staticmethod
def get_spaces(
credentials: TwitterCredentials,
space_ids: list[str],
user_ids: list[str],
expansions: list[SpaceExpansions],
space_fields: list[SpaceFields],
user_fields: list[TweetUserFields],
):
try:
client = tweepy.Client(
bearer_token=credentials.access_token.get_secret_value()
)
params = {
"ids": None if space_ids == [] else space_ids,
"user_ids": None if user_ids == [] else user_ids
}
params = (SpaceExpansionsBuilder(params)
.add_expansions(expansions)
.add_space_fields(space_fields)
.add_user_fields(user_fields)
.build())
print(" before space response")
response = cast(
Response,
client.get_spaces(**params)
)
ids = []
titles = []
included = IncludesSerializer.serialize(response.includes)
if response.data:
data = ResponseDataSerializer.serialize_list(response.data)
ids = [space["id"] for space in data]
titles = [space["title"] for space in data]
return data, included, ids, titles
raise Exception("No spaces found")
except tweepy.TweepyException:
raise
def run(
self,
input_data: Input,
*,
credentials: TwitterCredentials,
**kwargs,
) -> BlockOutput:
try:
data, included, ids, titles = self.get_spaces(
credentials,
input_data.space_ids,
input_data.user_ids,
input_data.expansions,
input_data.space_fields,
input_data.user_fields,
)
if ids:
yield "ids", ids
if titles:
yield "titles", titles
if data:
yield "data", data
if included:
yield "includes", included
except Exception as e:
yield "error", handle_tweepy_exception(e)
class TwitterGetSpaceByIdBlock(Block):
"""
Gets information about a single Twitter Space specified by Space ID
"""
class Input(SpaceExpansionInputs):
credentials: TwitterCredentialsInput = TwitterCredentialsField(
["spaces.read", "users.read", "offline.access"]
)
space_id: str = SchemaField(
description="Space ID to lookup",
placeholder="Enter Space ID",
required=True
)
class Output(BlockSchema):
# Common outputs
id: str = SchemaField(description="Space ID")
title: str = SchemaField(description="Space title")
host_ids: list[str] = SchemaField(description="Host ID")
# Complete outputs for advanced use
data: dict = SchemaField(description="Complete space data")
includes: dict = SchemaField(description="Additional data requested via expansions")
error: str = SchemaField(description="Error message if the request failed")
def __init__(self):
super().__init__(
id="c79700de-a62f-11ef-ab20-fb32bf9d5a9d",
description="This block retrieves information about a single Twitter Space.",
categories={BlockCategory.SOCIAL},
input_schema=TwitterGetSpaceByIdBlock.Input,
output_schema=TwitterGetSpaceByIdBlock.Output,
test_input={
"space_id": "1DXxyRYNejbKM",
"credentials": TEST_CREDENTIALS_INPUT,
"expansions": [],
"space_fields": [],
"user_fields": []
},
test_credentials=TEST_CREDENTIALS,
test_output=[
("id", "1DXxyRYNejbKM"),
("title", "Test Space"),
("host_id", "1234567"),
("data", {"id": "1DXxyRYNejbKM", "title": "Test Space", "host_id": "1234567"}),
("includes", {}),
("error", None)
],
)
@staticmethod
def get_space(
credentials: TwitterCredentials,
space_id: str,
expansions: list[SpaceExpansions],
space_fields: list[SpaceFields],
user_fields: list[TweetUserFields],
):
try:
client = tweepy.Client(
bearer_token=credentials.access_token.get_secret_value()
)
params = {
"id": space_id,
}
params = (SpaceExpansionsBuilder(params)
.add_expansions(expansions)
.add_space_fields(space_fields)
.add_user_fields(user_fields)
.build())
response = cast(
Response,
client.get_space(**params)
)
includes = {}
if response.includes:
for key, value in response.includes.items():
if isinstance(value, list):
includes[key] = [
item.data if hasattr(item, 'data') else item
for item in value
]
else:
includes[key] = value.data if hasattr(value, 'data') else value
data = {}
if response.data:
for key, value in response.data.items():
if isinstance(value, list):
data[key] = [
item.data if hasattr(item, 'data') else item
for item in value
]
else:
data[key] = value.data if hasattr(value, 'data') else value
return data, includes
raise Exception("Space not found")
except tweepy.TweepyException:
raise
def run(
self,
input_data: Input,
*,
credentials: TwitterCredentials,
**kwargs,
) -> BlockOutput:
try:
space_data, includes = self.get_space(
credentials,
input_data.space_id,
input_data.expansions,
input_data.space_fields,
input_data.user_fields,
)
# Common outputs
if space_data:
yield "id", space_data.get("id")
yield "title", space_data.get("title")
yield "host_ids", space_data.get("host_ids")
if space_data:
yield "data", space_data
if includes:
yield "includes", includes
except Exception as e:
yield "error", handle_tweepy_exception(e)
# Not tested yet, might have some problem
class TwitterGetSpaceBuyersBlock(Block):
"""
Gets list of users who purchased a ticket to the requested Space
"""
class Input(UserExpansionInputs):
credentials: TwitterCredentialsInput = TwitterCredentialsField(
["spaces.read", "users.read", "offline.access"]
)
space_id: str = SchemaField(
description="Space ID to lookup buyers for",
placeholder="Enter Space ID",
required=True
)
class Output(BlockSchema):
# Common outputs
buyer_ids: list[str] = SchemaField(description="List of buyer IDs")
usernames: list[str] = SchemaField(description="List of buyer usernames")
# Complete outputs for advanced use
data: list[dict] = SchemaField(description="Complete space buyers data")
includes: dict = SchemaField(description="Additional data requested via expansions")
error: str = SchemaField(description="Error message if the request failed")
def __init__(self):
super().__init__(
id="c1c121a8-a62f-11ef-8b0e-d7b85f96a46f",
description="This block retrieves a list of users who purchased tickets to a Twitter Space.",
categories={BlockCategory.SOCIAL},
input_schema=TwitterGetSpaceBuyersBlock.Input,
output_schema=TwitterGetSpaceBuyersBlock.Output,
test_input={
"space_id": "1DXxyRYNejbKM",
"credentials": TEST_CREDENTIALS_INPUT,
"expansions": [],
"user_fields": []
},
test_credentials=TEST_CREDENTIALS,
test_output=[
("buyer_ids", ["2244994945"]),
("usernames", ["testuser"]),
("data", {"id": "2244994945", "username": "testuser", "name": "Test User"}),
("includes", {}),
("error", None)
],
)
@staticmethod
def get_space_buyers(
credentials: TwitterCredentials,
space_id: str,
expansions: list[UserExpansions],
user_fields: list[TweetUserFields],
):
try:
client = tweepy.Client(
bearer_token=credentials.access_token.get_secret_value()
)
params = {
"id": space_id,
}
params = (UserExpansionsBuilder(params)
.add_expansions(expansions)
.add_user_fields(user_fields)
.build())
response = cast(
Response,
client.get_space_buyers(**params)
)
included = IncludesSerializer.serialize(response.includes)
if response.data:
data = ResponseDataSerializer.serialize_list(response.data)
buyer_ids = [buyer["id"] for buyer in data]
usernames = [buyer["username"] for buyer in data]
return data, included, buyer_ids, usernames
raise Exception("No buyers found for this Space")
except tweepy.TweepyException:
raise
def run(
self,
input_data: Input,
*,
credentials: TwitterCredentials,
**kwargs,
) -> BlockOutput:
try:
buyers_data, included, buyer_ids, usernames = self.get_space_buyers(
credentials,
input_data.space_id,
input_data.expansions,
input_data.user_fields
)
if buyer_ids:
yield "buyer_ids", buyer_ids
if usernames:
yield "usernames", usernames
if buyers_data:
yield "data", buyers_data
if included:
yield "includes", included
except Exception as e:
yield "error", handle_tweepy_exception(e)
class TwitterGetSpaceTweetsBlock(Block):
"""
Gets list of Tweets shared in the requested Space
"""
class Input(TweetExpansionInputs):
credentials: TwitterCredentialsInput = TwitterCredentialsField(
["spaces.read", "users.read", "offline.access"]
)
space_id: str = SchemaField(
description="Space ID to lookup tweets for",
placeholder="Enter Space ID",
required=True
)
class Output(BlockSchema):
# Common outputs
tweet_ids: list[str] = SchemaField(description="List of tweet IDs")
texts: list[str] = SchemaField(description="List of tweet texts")
# Complete outputs for advanced use
data: list[dict] = SchemaField(description="Complete space tweets data")
includes: dict = SchemaField(description="Additional data requested via expansions")
meta: dict = SchemaField(description="Response metadata")
error: str = SchemaField(description="Error message if the request failed")
def __init__(self):
super().__init__(
id="b69731e6-a62f-11ef-b2d4-1bf14dd6aee4",
description="This block retrieves tweets shared in a Twitter Space.",
categories={BlockCategory.SOCIAL},
input_schema=TwitterGetSpaceTweetsBlock.Input,
output_schema=TwitterGetSpaceTweetsBlock.Output,
test_input={
"space_id": "1DXxyRYNejbKM",
"credentials": TEST_CREDENTIALS_INPUT,
"expansions": [],
"media_fields": [],
"place_fields": [],
"poll_fields": [],
"tweet_fields": [],
"user_fields": []
},
test_credentials=TEST_CREDENTIALS,
test_output=[
("tweet_ids", ["1234567890"]),
("texts", ["Test tweet"]),
("data", {"tweets": [{"id": "1234567890", "text": "Test tweet"}]}),
("includes", {}),
("meta", {}),
("error", None)
],
)
@staticmethod
def get_space_tweets(
credentials: TwitterCredentials,
space_id: str,
expansions: list[TweetExpansions],
media_fields: list[TweetMediaFields],
place_fields: list[TweetPlaceFields],
poll_fields: list[TweetPollFields],
tweet_fields: list[TweetFields],
user_fields: list[TweetUserFields]
):
try:
client = tweepy.Client(
bearer_token=credentials.access_token.get_secret_value()
)
params = {
"id": space_id,
}
params = (TweetExpansionsBuilder(params)
.add_expansions(expansions)
.add_media_fields(media_fields)
.add_place_fields(place_fields)
.add_poll_fields(poll_fields)
.add_tweet_fields(tweet_fields)
.add_user_fields(user_fields)
.build())
response = cast(
Response,
client.get_space_tweets(**params)
)
included = IncludesSerializer.serialize(response.includes)
if response.data:
data = ResponseDataSerializer.serialize_list(response.data)
tweet_ids = [str(tweet["id"]) for tweet in data]
texts = [tweet["text"] for tweet in data]
meta = response.meta or {}
return data, included, tweet_ids, texts, meta
raise Exception("No tweets found for this Space")
except tweepy.TweepyException:
raise
def run(
self,
input_data: Input,
*,
credentials: TwitterCredentials,
**kwargs,
) -> BlockOutput:
try:
tweets_data, included, tweet_ids, texts, meta = self.get_space_tweets(
credentials,
input_data.space_id,
input_data.expansions,
input_data.media_fields,
input_data.place_fields,
input_data.poll_fields,
input_data.tweet_fields,
input_data.user_fields
)
if tweet_ids:
yield "tweet_ids", tweet_ids
if texts:
yield "texts", texts
if tweets_data:
yield "data", tweets_data
if included:
yield "includes", included
if meta:
yield "meta", meta
except Exception as e:
yield "error", handle_tweepy_exception(e)

View File

@ -0,0 +1,19 @@
import tweepy
def handle_tweepy_exception(e: Exception) -> str:
if isinstance(e, tweepy.BadRequest):
return f"Bad Request (400): {str(e)}"
elif isinstance(e, tweepy.Unauthorized):
return f"Unauthorized (401): {str(e)}"
elif isinstance(e, tweepy.Forbidden):
return f"Forbidden (403): {str(e)}"
elif isinstance(e, tweepy.NotFound):
return f"Not Found (404): {str(e)}"
elif isinstance(e, tweepy.TooManyRequests):
return f"Too Many Requests (429): {str(e)}"
elif isinstance(e, tweepy.TwitterServerError):
return f"Twitter Server Error (5xx): {str(e)}"
elif isinstance(e, tweepy.TweepyException):
return f"Tweepy Error: {str(e)}"
else:
return f"Unexpected error: {str(e)}"

View File

@ -0,0 +1,334 @@
from typing import cast
from backend.blocks.twitter._serializer import IncludesSerializer, ResponseDataSerializer
import tweepy
from tweepy.client import Response
from backend.data.block import Block, BlockCategory, BlockOutput, BlockSchema
from backend.data.model import SchemaField
from backend.blocks.twitter._builders import TweetExpansionsBuilder
from backend.blocks.twitter._types import TweetExpansions, TweetFields, TweetMediaFields, TweetPlaceFields, TweetPollFields, TweetUserFields, TweetExpansionInputs
from backend.blocks.twitter.tweepy_exceptions import handle_tweepy_exception
from backend.blocks.twitter._auth import (
TEST_CREDENTIALS,
TEST_CREDENTIALS_INPUT,
TwitterCredentials,
TwitterCredentialsField,
TwitterCredentialsInput,
)
class TwitterBookmarkTweetBlock(Block):
"""
Bookmark a tweet on Twitter
"""
class Input(BlockSchema):
credentials: TwitterCredentialsInput = TwitterCredentialsField(
["tweet.read","bookmark.write", "users.read", "offline.access"]
)
tweet_id: str = SchemaField(
description="ID of the tweet to bookmark",
placeholder="Enter tweet ID",
)
class Output(BlockSchema):
success: bool = SchemaField(description="Whether the bookmark was successful")
error: str = SchemaField(description="Error message if the bookmark failed")
def __init__(self):
super().__init__(
id="f33d67be-a62f-11ef-a797-ff83ec29ee8e",
description="This block bookmarks a tweet on Twitter.",
categories={BlockCategory.SOCIAL},
input_schema=TwitterBookmarkTweetBlock.Input,
output_schema=TwitterBookmarkTweetBlock.Output,
test_input={
"tweet_id": "1234567890",
"credentials": TEST_CREDENTIALS_INPUT,
},
test_credentials=TEST_CREDENTIALS,
test_output=[
("success", True),
],
)
@staticmethod
def bookmark_tweet(
credentials: TwitterCredentials,
tweet_id: str,
):
try:
client = tweepy.Client(
bearer_token=credentials.access_token.get_secret_value()
)
client.bookmark(tweet_id)
return True
except tweepy.TweepyException:
raise
def run(
self,
input_data: Input,
*,
credentials: TwitterCredentials,
**kwargs,
) -> BlockOutput:
try:
success = self.bookmark_tweet(credentials, input_data.tweet_id)
yield "success", success
except Exception as e:
yield "error", handle_tweepy_exception(e)
class TwitterGetBookmarkedTweetsBlock(Block):
"""
Get All your bookmarked tweets from Twitter
"""
class Input(TweetExpansionInputs):
credentials: TwitterCredentialsInput = TwitterCredentialsField(
["tweet.read", "bookmark.read", "users.read", "offline.access"]
)
max_results: int = SchemaField(
description="Maximum number of results to return (1-100)",
placeholder="Enter max results",
default=10,
advanced=True
)
pagination_token: str = SchemaField(
description="Token for pagination",
placeholder="Enter pagination token",
default = "",
advanced=True,
)
class Output(BlockSchema):
# Common Outputs that user commonly uses
id : list[str] = SchemaField(description="All Tweet IDs")
text : list[str] = SchemaField(description="All Tweet texts")
userId: list[str] = SchemaField(description="IDs of the tweet authors")
userName: list[str] = SchemaField(description="Usernames of the tweet authors")
# Complete Outputs for advanced use
data : list[dict] = SchemaField(description="Complete Tweet data")
included: dict = SchemaField(description="Additional data that you have requested (Optional) via Expansions field")
meta : dict = SchemaField(description="Provides metadata such as pagination info (next_token) or result counts")
next_token : str = SchemaField(description="Next token for pagination")
error: str = SchemaField(description="Error message if the request failed")
def __init__(self):
super().__init__(
id="ed26783e-a62f-11ef-9a21-c77c57dd8a1f",
description="This block retrieves bookmarked tweets from Twitter.",
categories={BlockCategory.SOCIAL},
input_schema=TwitterGetBookmarkedTweetsBlock.Input,
output_schema=TwitterGetBookmarkedTweetsBlock.Output,
test_input={
"max_results": 10,
"pagination_token": "",
"expansions": [],
"media_fields": [],
"place_fields": [],
"poll_fields": [],
"tweet_fields": [],
"user_fields": [],
"credentials": TEST_CREDENTIALS_INPUT,
},
test_credentials=TEST_CREDENTIALS,
test_output=[
("id", ["1234567890"]),
("text", ["Test tweet"]),
("userId", ["12345"]),
("userName", ["testuser"]),
("data", [{"id": "1234567890", "text": "Test tweet"}]),
("included", {"users": [{"id": "12345", "username": "testuser"}]}),
("meta", {"result_count": 1}),
("next_token", "next_token_value")
],
)
@staticmethod
def get_bookmarked_tweets(
credentials: TwitterCredentials,
max_results: int,
pagination_token: str,
expansions: list[TweetExpansions],
media_fields: list[TweetMediaFields],
place_fields: list[TweetPlaceFields],
poll_fields: list[TweetPollFields],
tweet_fields: list[TweetFields],
user_fields: list[TweetUserFields]
):
try:
client = tweepy.Client(
bearer_token=credentials.access_token.get_secret_value()
)
params = {
"max_results": max_results,
"pagination_token": None if pagination_token == "" else pagination_token,
}
params = (TweetExpansionsBuilder(params)
.add_expansions(expansions)
.add_media_fields(media_fields)
.add_place_fields(place_fields)
.add_poll_fields(poll_fields)
.add_tweet_fields(tweet_fields)
.add_user_fields(user_fields)
.build())
response = cast(
Response,
client.get_bookmarks(
**params
),
)
meta = {}
tweet_ids = []
tweet_texts = []
user_ids = []
user_names = []
next_token = None
if response.meta:
meta = response.meta
next_token = meta.get("next_token")
included = IncludesSerializer.serialize(response.includes)
data = ResponseDataSerializer.serialize_list(response.data)
if response.data:
tweet_ids = [str(tweet.id) for tweet in response.data]
tweet_texts = [tweet.text for tweet in response.data]
if "users" in included:
for user in included["users"]:
user_ids.append(str(user["id"]))
user_names.append(user["username"])
return tweet_ids, tweet_texts, user_ids, user_names, data, included, meta, next_token
raise Exception("No bookmarked tweets found")
except tweepy.TweepyException:
raise
def run(
self,
input_data: Input,
*,
credentials: TwitterCredentials,
**kwargs,
) -> BlockOutput:
try:
ids, texts, user_ids, user_names, data, included, meta, next_token = self.get_bookmarked_tweets(
credentials,
input_data.max_results,
input_data.pagination_token,
input_data.expansions,
input_data.media_fields,
input_data.place_fields,
input_data.poll_fields,
input_data.tweet_fields,
input_data.user_fields
)
if ids:
yield "id", ids
if texts:
yield "text", texts
if user_ids:
yield "userId", user_ids
if user_names:
yield "userName", user_names
if data:
yield "data", data
if included:
yield "included", included
if meta:
yield "meta", meta
if next_token:
yield "next_token", next_token
except Exception as e:
yield "error", handle_tweepy_exception(e)
class TwitterRemoveBookmarkTweetBlock(Block):
"""
Remove a bookmark for a tweet on Twitter
"""
class Input(BlockSchema):
credentials: TwitterCredentialsInput = TwitterCredentialsField(
["tweet.read", "bookmark.write", "users.read", "offline.access"]
)
tweet_id: str = SchemaField(
description="ID of the tweet to remove bookmark from",
placeholder="Enter tweet ID",
)
class Output(BlockSchema):
success: bool = SchemaField(
description="Whether the bookmark was successfully removed"
)
error: str = SchemaField(
description="Error message if the bookmark removal failed"
)
def __init__(self):
super().__init__(
id="e4100684-a62f-11ef-9be9-770cb41a2616",
description="This block removes a bookmark from a tweet on Twitter.",
categories={BlockCategory.SOCIAL},
input_schema=TwitterRemoveBookmarkTweetBlock.Input,
output_schema=TwitterRemoveBookmarkTweetBlock.Output,
test_input={
"tweet_id": "1234567890",
"credentials": TEST_CREDENTIALS_INPUT,
},
test_credentials=TEST_CREDENTIALS,
test_output=[
("success", True),
],
test_mock={
"remove_bookmark_tweet": lambda *args, **kwargs: True
}
)
@staticmethod
def remove_bookmark_tweet(
credentials: TwitterCredentials,
tweet_id: str,
):
try:
client = tweepy.Client(
bearer_token=credentials.access_token.get_secret_value()
)
client.remove_bookmark(tweet_id)
return True
except tweepy.TweepyException:
raise
def run(
self,
input_data: Input,
*,
credentials: TwitterCredentials,
**kwargs,
) -> BlockOutput:
try:
success = self.remove_bookmark_tweet(credentials, input_data.tweet_id)
yield "success", success
except Exception as e:
yield "error", handle_tweepy_exception(e)

View File

@ -0,0 +1,150 @@
import tweepy
from backend.data.block import Block, BlockCategory, BlockOutput, BlockSchema
from backend.data.model import SchemaField
from backend.blocks.twitter.tweepy_exceptions import handle_tweepy_exception
from backend.blocks.twitter._auth import (
TEST_CREDENTIALS,
TEST_CREDENTIALS_INPUT,
TwitterCredentials,
TwitterCredentialsField,
TwitterCredentialsInput,
)
class TwitterHideReplyBlock(Block):
"""
Hides a reply of one of your tweets
"""
class Input(BlockSchema):
credentials: TwitterCredentialsInput = TwitterCredentialsField(
["tweet.read", "tweet.moderate.write", "users.read", "offline.access"]
)
tweet_id: str = SchemaField(
description="ID of the tweet reply to hide",
placeholder="Enter tweet ID",
)
class Output(BlockSchema):
success: bool = SchemaField(description="Whether the operation was successful")
error: str = SchemaField(description="Error message if the request failed")
def __init__(self):
super().__init__(
id="07d58b3e-a630-11ef-a030-93701d1a465e",
description="This block hides a reply to a tweet.",
categories={BlockCategory.SOCIAL},
input_schema=TwitterHideReplyBlock.Input,
output_schema=TwitterHideReplyBlock.Output,
test_input={
"tweet_id": "1234567890",
"credentials": TEST_CREDENTIALS_INPUT,
},
test_credentials=TEST_CREDENTIALS,
test_output=[
("success", True),
],
)
@staticmethod
def hide_reply(
credentials: TwitterCredentials,
tweet_id: str,
):
try:
client = tweepy.Client(
bearer_token=credentials.access_token.get_secret_value()
)
client.hide_reply(id=tweet_id, user_auth=False)
return True
except tweepy.TweepyException:
raise
def run(
self,
input_data: Input,
*,
credentials: TwitterCredentials,
**kwargs,
) -> BlockOutput:
try:
success = self.hide_reply(
credentials,
input_data.tweet_id,
)
yield "success", success
except Exception as e:
yield "error", handle_tweepy_exception(e)
class TwitterUnhideReplyBlock(Block):
"""
Unhides a reply to a tweet
"""
class Input(BlockSchema):
credentials: TwitterCredentialsInput = TwitterCredentialsField(
["tweet.read", "tweet.moderate.write", "users.read", "offline.access"]
)
tweet_id: str = SchemaField(
description="ID of the tweet reply to unhide",
placeholder="Enter tweet ID",
)
class Output(BlockSchema):
success: bool = SchemaField(description="Whether the operation was successful")
error: str = SchemaField(description="Error message if the request failed")
def __init__(self):
super().__init__(
id="fcf9e4e4-a62f-11ef-9d85-57d3d06b616a",
description="This block unhides a reply to a tweet.",
categories={BlockCategory.SOCIAL},
input_schema=TwitterUnhideReplyBlock.Input,
output_schema=TwitterUnhideReplyBlock.Output,
test_input={
"tweet_id": "1234567890",
"credentials": TEST_CREDENTIALS_INPUT,
},
test_credentials=TEST_CREDENTIALS,
test_output=[
("success", True),
],
)
@staticmethod
def unhide_reply(
credentials: TwitterCredentials,
tweet_id: str,
):
try:
client = tweepy.Client(
bearer_token=credentials.access_token.get_secret_value()
)
client.unhide_reply(id=tweet_id, user_auth=False)
return True
except tweepy.TweepyException:
raise
def run(
self,
input_data: Input,
*,
credentials: TwitterCredentials,
**kwargs,
) -> BlockOutput:
try:
success = self.unhide_reply(
credentials,
input_data.tweet_id,
)
yield "success", success
except Exception as e:
yield "error", handle_tweepy_exception(e)

View File

@ -0,0 +1,551 @@
from typing import cast
from backend.blocks.twitter._serializer import IncludesSerializer, ResponseDataSerializer
import tweepy
from tweepy.client import Response
from backend.data.block import Block, BlockCategory, BlockOutput, BlockSchema
from backend.data.model import SchemaField
from backend.blocks.twitter._builders import TweetExpansionsBuilder, UserExpansionsBuilder
from backend.blocks.twitter._types import TweetExpansionInputs, TweetExpansions, TweetFields, TweetMediaFields, TweetPlaceFields, TweetPollFields, TweetUserFields, UserExpansionInputs, UserExpansions
from backend.blocks.twitter.tweepy_exceptions import handle_tweepy_exception
from backend.blocks.twitter._auth import (
TEST_CREDENTIALS,
TEST_CREDENTIALS_INPUT,
TwitterCredentials,
TwitterCredentialsField,
TwitterCredentialsInput,
)
class TwitterLikeTweetBlock(Block):
"""
Likes a tweet
"""
class Input(BlockSchema):
credentials: TwitterCredentialsInput = TwitterCredentialsField(
["tweet.read", "like.write", "users.read", "offline.access"]
)
tweet_id: str = SchemaField(
description="ID of the tweet to like",
placeholder="Enter tweet ID",
)
class Output(BlockSchema):
success: bool = SchemaField(description="Whether the operation was successful")
error: str = SchemaField(description="Error message if the request failed")
def __init__(self):
super().__init__(
id="4d0b4c5c-a630-11ef-8e08-1b14c507b347",
description="This block likes a tweet.",
categories={BlockCategory.SOCIAL},
input_schema=TwitterLikeTweetBlock.Input,
output_schema=TwitterLikeTweetBlock.Output,
test_input={
"tweet_id": "1234567890",
"credentials": TEST_CREDENTIALS_INPUT,
},
test_credentials=TEST_CREDENTIALS,
test_output=[
("success", True),
],
)
@staticmethod
def like_tweet(
credentials: TwitterCredentials,
tweet_id: str,
):
try:
client = tweepy.Client(
bearer_token=credentials.access_token.get_secret_value()
)
client.like(tweet_id=tweet_id, user_auth=False)
return True
except tweepy.TweepyException:
raise
def run(
self,
input_data: Input,
*,
credentials: TwitterCredentials,
**kwargs,
) -> BlockOutput:
try:
success = self.like_tweet(
credentials,
input_data.tweet_id,
)
yield "success", success
except Exception as e:
yield "error", handle_tweepy_exception(e)
class TwitterGetLikingUsersBlock(Block):
"""
Gets information about users who liked a one of your tweet
"""
class Input(UserExpansionInputs):
credentials: TwitterCredentialsInput = TwitterCredentialsField(
["tweet.read", "users.read","like.read", "offline.access"]
)
tweet_id: str = SchemaField(
description="ID of the tweet to get liking users for",
placeholder="Enter tweet ID",
)
max_results: int = SchemaField(
description="Maximum number of results to return (1-100)",
placeholder="Enter max results",
default=10,
advanced=True,
)
pagination_token: str = SchemaField(
description="Token for getting next/previous page of results",
placeholder="Enter pagination token",
default="",
advanced=True,
)
class Output(BlockSchema):
# Common Outputs that user commonly uses
id : list[str] = SchemaField(description="All User IDs who liked the tweet")
username : list[str] = SchemaField(description="All User usernames who liked the tweet")
next_token : str = SchemaField(description="Next token for pagination")
# Complete Outputs for advanced use
data : list[dict] = SchemaField(description="Complete Tweet data")
included : dict = SchemaField(description="Additional data that you have requested (Optional) via Expansions field")
meta : dict = SchemaField(description="Provides metadata such as pagination info (next_token) or result counts")
# error
error: str = SchemaField(description="Error message if the request failed")
def __init__(self):
super().__init__(
id="34275000-a630-11ef-b01e-5f00d9077c08",
description="This block gets information about users who liked a tweet.",
categories={BlockCategory.SOCIAL},
input_schema=TwitterGetLikingUsersBlock.Input,
output_schema=TwitterGetLikingUsersBlock.Output,
test_input={
"tweet_id": "1234567890",
"max_results": 10,
"pagination_token": "",
"credentials": TEST_CREDENTIALS_INPUT,
"expansions": [],
"tweet_fields": [],
"user_fields": []
},
test_credentials=TEST_CREDENTIALS,
test_output=[
("id", ["12345", "67890"]),
("username", ["user1", "user2"]),
("data", [
{
"id": "12345",
"username": "user1"
},
{
"id": "67890",
"username": "user2"
}
]),
("included", {}),
("meta", {
"result_count": 2,
"next_token": "next_token_value"
}),
("next_token", "next_token_value")
],
test_mock={
"get_liking_users": lambda *args, **kwargs: (
["12345", "67890"],
["user1", "user2"],
[{"id": "12345", "username": "user1"}, {"id": "67890", "username": "user2"}],
{},
{"result_count": 2, "next_token": "next_token_value"},
"next_token_value"
)
}
)
@staticmethod
def get_liking_users(
credentials: TwitterCredentials,
tweet_id: str,
max_results: int,
pagination_token: str,
expansions: list[UserExpansions],
tweet_fields: list[TweetFields],
user_fields: list[TweetUserFields]
):
try:
client = tweepy.Client(
bearer_token=credentials.access_token.get_secret_value()
)
params = {
"id": tweet_id,
"max_results": max_results,
"pagination_token": None if pagination_token == "" else pagination_token,
"user_auth": False
}
params = (UserExpansionsBuilder(params)
.add_expansions(expansions)
.add_tweet_fields(tweet_fields)
.add_user_fields(user_fields)
.build())
response = cast(
Response,
client.get_liking_users(**params)
)
if not response.data and not response.meta:
raise Exception("No liking users found")
meta = {}
user_ids = []
usernames = []
next_token = None
if response.meta:
meta = response.meta
next_token = meta.get("next_token")
included = IncludesSerializer.serialize(response.includes)
data = ResponseDataSerializer.serialize_list(response.data)
if response.data:
user_ids = [str(user.id) for user in response.data]
usernames = [user.username for user in response.data]
return user_ids, usernames, data, included, meta, next_token
raise Exception("No liking users found")
except tweepy.TweepyException:
raise
def run(
self,
input_data: Input,
*,
credentials: TwitterCredentials,
**kwargs,
) -> BlockOutput:
try:
ids, usernames, data, included, meta, next_token = self.get_liking_users(
credentials,
input_data.tweet_id,
input_data.max_results,
input_data.pagination_token,
input_data.expansions,
input_data.tweet_fields,
input_data.user_fields
)
if ids:
yield "id", ids
if usernames:
yield "username", usernames
if next_token:
yield "next_token", next_token
if data:
yield "data", data
if included:
yield "included", included
if meta:
yield "meta", meta
except Exception as e:
yield "error", handle_tweepy_exception(e)
class TwitterGetLikedTweetsBlock(Block):
"""
Gets information about tweets liked by you
"""
class Input(TweetExpansionInputs):
credentials: TwitterCredentialsInput = TwitterCredentialsField(
["tweet.read", "users.read","like.read", "offline.access"]
)
user_id: str = SchemaField(
description="ID of the user to get liked tweets for",
placeholder="Enter user ID",
)
max_results: int = SchemaField(
description="Maximum number of results to return (5-100)",
placeholder="100",
default=10,
advanced=True
)
pagination_token: str = SchemaField(
description="Token for getting next/previous page of results",
placeholder="Enter pagination token",
default="",
advanced=True,
)
class Output(BlockSchema):
# Common Outputs that user commonly uses
ids : list[str] = SchemaField(description="All Tweet IDs")
texts : list[str] = SchemaField(description="All Tweet texts")
userIds: list[str] = SchemaField(description="List of user ids that authored the tweets")
userNames: list[str] = SchemaField(description="List of user names that authored the tweets")
next_token : str = SchemaField(description="Next token for pagination")
# Complete Outputs for advanced use
data : list[dict] = SchemaField(description="Complete Tweet data")
included : dict = SchemaField(description="Additional data that you have requested (Optional) via Expansions field")
meta : dict = SchemaField(description="Provides metadata such as pagination info (next_token) or result counts")
# error
error: str = SchemaField(description="Error message if the request failed")
def __init__(self):
super().__init__(
id="292e7c78-a630-11ef-9f40-df5dffaca106",
description="This block gets information about tweets liked by a user.",
categories={BlockCategory.SOCIAL},
input_schema=TwitterGetLikedTweetsBlock.Input,
output_schema=TwitterGetLikedTweetsBlock.Output,
test_input={
"user_id": "1234567890",
"max_results": 10,
"pagination_token": "",
"credentials": TEST_CREDENTIALS_INPUT,
"expansions": [],
"media_fields": [],
"place_fields": [],
"poll_fields": [],
"tweet_fields": [],
"user_fields": []
},
test_credentials=TEST_CREDENTIALS,
test_output=[
("ids", ["12345", "67890"]),
("texts", ["Tweet 1", "Tweet 2"]),
("userIds", ["67890", "67891"]),
("userNames", ["testuser1", "testuser2"]),
("data", [
{
"id": "12345",
"text": "Tweet 1"
},
{
"id": "67890",
"text": "Tweet 2"
}
]),
("included", {
"users": [
{"id": "67890", "username": "testuser1"},
{"id": "67891", "username": "testuser2"}
]
}),
("meta", {
"result_count": 2,
"next_token": "next_token_value"
}),
("next_token", "next_token_value")
],
test_mock={
"get_liked_tweets": lambda *args, **kwargs: (
["12345", "67890"],
["Tweet 1", "Tweet 2"],
["67890", "67891"],
["testuser1", "testuser2"],
[{"id": "12345", "text": "Tweet 1"}, {"id": "67890", "text": "Tweet 2"}],
{"users": [{"id": "67890", "username": "testuser1"}, {"id": "67891", "username": "testuser2"}]},
{"result_count": 2, "next_token": "next_token_value"},
"next_token_value"
)
}
)
@staticmethod
def get_liked_tweets(
credentials: TwitterCredentials,
user_id: str,
max_results: int,
pagination_token: str,
expansions: list[TweetExpansions],
media_fields: list[TweetMediaFields],
place_fields: list[TweetPlaceFields],
poll_fields: list[TweetPollFields],
tweet_fields: list[TweetFields],
user_fields: list[TweetUserFields]
):
try:
client = tweepy.Client(
bearer_token=credentials.access_token.get_secret_value()
)
params = {
"id": user_id,
"max_results": max_results,
"pagination_token":None if pagination_token == "" else pagination_token,
"user_auth": False
}
params = (TweetExpansionsBuilder(params)
.add_expansions(expansions)
.add_media_fields(media_fields)
.add_place_fields(place_fields)
.add_poll_fields(poll_fields)
.add_tweet_fields(tweet_fields)
.add_user_fields(user_fields)
.build())
response = cast(
Response,
client.get_liked_tweets(**params)
)
if not response.data and not response.meta:
raise Exception("No liked tweets found")
meta = {}
tweet_ids = []
tweet_texts = []
user_ids = []
user_names = []
next_token = None
if response.meta:
meta = response.meta
next_token = meta.get("next_token")
included = IncludesSerializer.serialize(response.includes)
data = ResponseDataSerializer.serialize_list(response.data)
if response.data:
tweet_ids = [str(tweet.id) for tweet in response.data]
tweet_texts = [tweet.text for tweet in response.data]
if "users" in response.includes:
user_ids = [str(user["id"]) for user in response.includes["users"]]
user_names = [user["username"] for user in response.includes["users"]]
return tweet_ids, tweet_texts, user_ids, user_names, data, included, meta, next_token
raise Exception("No liked tweets found")
except tweepy.TweepyException:
raise
def run(
self,
input_data: Input,
*,
credentials: TwitterCredentials,
**kwargs,
) -> BlockOutput:
try:
ids, texts, user_ids, user_names, data, included, meta, next_token = self.get_liked_tweets(
credentials,
input_data.user_id,
input_data.max_results,
input_data.pagination_token,
input_data.expansions,
input_data.media_fields,
input_data.place_fields,
input_data.poll_fields,
input_data.tweet_fields,
input_data.user_fields
)
if ids:
yield "ids", ids
if texts:
yield "texts", texts
if user_ids:
yield "userIds", user_ids
if user_names:
yield "userNames", user_names
if next_token:
yield "next_token", next_token
if data:
yield "data", data
if included:
yield "included", included
if meta:
yield "meta", meta
except Exception as e:
yield "error", handle_tweepy_exception(e)
class TwitterUnlikeTweetBlock(Block):
"""
Unlikes a tweet that was previously liked
"""
class Input(BlockSchema):
credentials: TwitterCredentialsInput = TwitterCredentialsField(
["tweet.read", "like.write", "users.read", "offline.access"]
)
tweet_id: str = SchemaField(
description="ID of the tweet to unlike",
placeholder="Enter tweet ID",
)
class Output(BlockSchema):
success: bool = SchemaField(description="Whether the operation was successful")
error: str = SchemaField(description="Error message if the request failed")
def __init__(self):
super().__init__(
id="1ed5eab8-a630-11ef-8e21-cbbbc80cbb85",
description="This block unlikes a tweet.",
categories={BlockCategory.SOCIAL},
input_schema=TwitterUnlikeTweetBlock.Input,
output_schema=TwitterUnlikeTweetBlock.Output,
test_input={
"tweet_id": "1234567890",
"credentials": TEST_CREDENTIALS_INPUT,
},
test_credentials=TEST_CREDENTIALS,
test_output=[
("success", True),
],
)
@staticmethod
def unlike_tweet(
credentials: TwitterCredentials,
tweet_id: str,
):
try:
client = tweepy.Client(
bearer_token=credentials.access_token.get_secret_value()
)
client.unlike(tweet_id=tweet_id, user_auth=False)
return True
except tweepy.TweepyException:
raise
def run(
self,
input_data: Input,
*,
credentials: TwitterCredentials,
**kwargs,
) -> BlockOutput:
try:
success = self.unlike_tweet(
credentials,
input_data.tweet_id,
)
yield "success", success
except Exception as e:
yield "error", handle_tweepy_exception(e)

View File

@ -0,0 +1,535 @@
from typing import cast
from backend.blocks.twitter._serializer import IncludesSerializer, ResponseDataSerializer
import tweepy
from tweepy.client import Response
from backend.blocks.twitter._types import TweetExpansionInputs, TweetTimeWindowInputs
from backend.blocks.twitter.tweepy_exceptions import handle_tweepy_exception
from backend.data.block import Block, BlockCategory, BlockOutput, BlockSchema
from backend.data.model import SchemaField
from backend.blocks.twitter._builders import TweetExpansionsBuilder,TweetDurationBuilder, TweetPostBuilder, TweetSearchBuilder
from backend.blocks.twitter._types import TweetExpansions, TweetMediaFields, TweetPlaceFields, TweetPollFields, TweetReplySettings, TweetFields, TweetUserFields
from backend.blocks.twitter._auth import (
TEST_CREDENTIALS,
TEST_CREDENTIALS_INPUT,
TwitterCredentials,
TwitterCredentialsField,
TwitterCredentialsInput,
)
class TwitterPostTweetBlock(Block):
"""
Create a tweet on Twitter with the option to include one additional element such as a media, quote, or deep link except poll.
"""
class Input(BlockSchema):
credentials: TwitterCredentialsInput = TwitterCredentialsField(
["tweet.read", "tweet.write", "users.read", "offline.access"]
)
tweet_text: str = SchemaField(
description="Text of the tweet to post [It's Optional if you want to add media, quote, or deep link]",
placeholder="Enter your tweet",
advanced=False,
default=""
)
media_ids: list = SchemaField(
description="List of media IDs to attach to the tweet, [ex - 1455952740635586573]",
placeholder="Enter media IDs",
default=[],
)
media_tagged_user_ids: list = SchemaField(
description="List of user IDs to tag in media, [ex - 1455952740635586573]",
placeholder="Enter media tagged user IDs",
default=[],
)
direct_message_deep_link: str = SchemaField(
description="Link directly to a Direct Message conversation with an account [ex - https://twitter.com/messages/compose?recipient_id={your_id}]",
placeholder="Enter direct message deep link",
default="",
)
poll_options: list = SchemaField(
description="List of poll options",
placeholder="Enter poll options",
default=[],
)
poll_duration_minutes: int = SchemaField(
description="Duration of the poll in minutes",
placeholder="Enter poll duration in minutes",
default=0,
)
for_super_followers_only: bool = SchemaField(
description="Tweet exclusively for Super Followers",
placeholder="Enter for super followers only",
default=False,
)
place_id: str = SchemaField(
description="Adds optional location information to a tweet if geo settings are enabled in your profile.",
placeholder="Enter place ID",
default="",
)
quote_tweet_id: str = SchemaField(
description="Link to the Tweet being quoted, [ex- 1455953449422516226]",
advanced=True,
placeholder="Enter quote tweet ID",
default=""
)
exclude_reply_user_ids: list = SchemaField(
description="User IDs to exclude from reply Tweet thread. [ex - 6253282] ",
placeholder="Enter user IDs to exclude",
advanced=True,
default=[],
)
in_reply_to_tweet_id: str = SchemaField(
description="Tweet ID being replied to. Please note that in_reply_to_tweet_id needs to be in the request if exclude_reply_user_ids is present",
default="",
placeholder="Enter in reply to tweet ID",
advanced=True
)
reply_settings: TweetReplySettings = SchemaField(
description="Who can reply to the Tweet (mentionedUsers or following)",
placeholder="Enter reply settings",
advanced=True,
default=TweetReplySettings.all_users,
)
class Output(BlockSchema):
tweet_id: str = SchemaField(description="ID of the created tweet")
tweet_url: str = SchemaField(description="URL to the tweet")
error: str = SchemaField(
description="Error message if the tweet posting failed"
)
def __init__(self):
super().__init__(
id="7bb0048a-a630-11ef-aeb8-abc0dadb9b12",
description="This block posts a tweet on Twitter.",
categories={BlockCategory.SOCIAL},
input_schema=TwitterPostTweetBlock.Input,
output_schema=TwitterPostTweetBlock.Output,
test_input={
"tweet_text": "This is a test tweet.",
"credentials": TEST_CREDENTIALS_INPUT,
"direct_message_deep_link": "",
"for_super_followers_only": False,
"place_id": "",
"media_ids": [],
"media_tagged_user_ids": [],
"quote_tweet_id": "",
"exclude_reply_user_ids": [],
"in_reply_to_tweet_id": "",
"reply_settings": TweetReplySettings.all_users,
},
test_credentials=TEST_CREDENTIALS,
test_output=[
("tweet_id", "1234567890"),
("tweet_url", "https://twitter.com/user/status/1234567890"),
],
test_mock={
"post_tweet": lambda *args, **kwargs: (
"1234567890",
"https://twitter.com/user/status/1234567890",
)
},
)
@staticmethod
def post_tweet(
credentials: TwitterCredentials,
input_txt: str ,
media_ids: list,
media_tagged_user_ids: list,
direct_message_deep_link: str ,
for_super_followers_only: bool ,
place_id: str ,
poll_options: list,
poll_duration_minutes: int,
quote_tweet_id: str ,
exclude_reply_user_ids: list ,
in_reply_to_tweet_id: str ,
reply_settings: TweetReplySettings,
):
try:
client = tweepy.Client(
bearer_token=credentials.access_token.get_secret_value()
)
params = (TweetPostBuilder()
.add_text(input_txt)
.add_media(media_ids, media_tagged_user_ids)
.add_deep_link(direct_message_deep_link)
.add_super_followers(for_super_followers_only)
.add_poll_options(poll_options)
.add_poll_duration(poll_duration_minutes)
.add_place(place_id)
.add_quote(quote_tweet_id)
.add_reply_settings(exclude_reply_user_ids, in_reply_to_tweet_id, reply_settings)
.build())
tweet = cast(
Response,
client.create_tweet(**params)
)
if not tweet.data:
raise Exception("Failed to create tweet")
tweet_id = tweet.data["id"]
tweet_url = f"https://twitter.com/user/status/{tweet_id}"
return str(tweet_id), tweet_url
except tweepy.TweepyException:
raise
except Exception as e:
print(f"Unexpected error: {str(e)}")
raise
def run(
self,
input_data: Input,
*,
credentials: TwitterCredentials,
**kwargs,
) -> BlockOutput:
try:
tweet_id, tweet_url = self.post_tweet(
credentials,
input_data.tweet_text,
input_data.media_ids,
input_data.media_tagged_user_ids,
input_data.direct_message_deep_link,
input_data.for_super_followers_only,
input_data.place_id,
input_data.poll_options,
input_data.poll_duration_minutes,
input_data.quote_tweet_id,
input_data.exclude_reply_user_ids,
input_data.in_reply_to_tweet_id,
input_data.reply_settings,
)
yield "tweet_id", tweet_id
yield "tweet_url", tweet_url
except Exception as e:
yield "error", handle_tweepy_exception(e)
class TwitterDeleteTweetBlock(Block):
"""
Deletes a tweet on Twitter using twitter Id
"""
class Input(BlockSchema):
credentials: TwitterCredentialsInput = TwitterCredentialsField(
["tweet.read", "tweet.write", "users.read", "offline.access"]
)
tweet_id: str = SchemaField(
description="ID of the tweet to delete",
placeholder="Enter tweet ID",
)
class Output(BlockSchema):
success: bool = SchemaField(
description="Whether the tweet was successfully deleted"
)
error: str = SchemaField(
description="Error message if the tweet deletion failed"
)
def __init__(self):
super().__init__(
id="761babf0-a630-11ef-a03d-abceb082f58f",
description="This block deletes a tweet on Twitter.",
categories={BlockCategory.SOCIAL},
input_schema=TwitterDeleteTweetBlock.Input,
output_schema=TwitterDeleteTweetBlock.Output,
test_input={
"tweet_id": "1234567890",
"credentials": TEST_CREDENTIALS_INPUT,
},
test_credentials=TEST_CREDENTIALS,
test_output=[("success", True)],
test_mock={
"delete_tweet": lambda *args, **kwargs: True
},
)
@staticmethod
def delete_tweet(credentials: TwitterCredentials, tweet_id: str):
try:
client = tweepy.Client(
bearer_token=credentials.access_token.get_secret_value()
)
client.delete_tweet(id=tweet_id, user_auth=False)
return True
except tweepy.TweepyException:
raise
except Exception as e:
print(f"Unexpected error: {str(e)}")
raise
def run(
self,
input_data: Input,
*,
credentials: TwitterCredentials,
**kwargs,
) -> BlockOutput:
try:
success = self.delete_tweet(
credentials,
input_data.tweet_id,
)
yield "success", success
except Exception as e:
yield "error", handle_tweepy_exception(e)
class TwitterSearchRecentTweetsBlock(Block):
"""
Searches all public Tweets in Twitter history
"""
class Input(TweetExpansionInputs, TweetTimeWindowInputs):
credentials: TwitterCredentialsInput = TwitterCredentialsField(
["tweet.read", "users.read", "offline.access"]
)
query: str = SchemaField(
description="Search query (up to 1024 characters)",
placeholder="Enter search query",
)
max_results: int = SchemaField(
description="Maximum number of results per page (10-500)",
placeholder="Enter max results",
default=10,
advanced=True,
)
pagination: str = SchemaField(
description="Token for pagination",
default="",
placeholder="Enter pagination token",
advanced=True,
)
class Output(BlockSchema):
# Common Outputs that user commonly uses
tweet_ids : list[str] = SchemaField(description="All Tweet IDs")
tweet_texts : list[str] = SchemaField(description="All Tweet texts")
next_token : str = SchemaField(description="Next token for pagination")
# Complete Outputs for advanced use
data : list[dict] = SchemaField(description="Complete Tweet data")
included : dict = SchemaField(description="Additional data that you have requested (Optional) via Expansions field")
meta : dict = SchemaField(description="Provides metadata such as pagination info (next_token) or result counts")
# error
error: str = SchemaField(description="Error message if the request failed")
def __init__(self):
super().__init__(
id="53e5cf8e-a630-11ef-ba85-df6d666fa5d5",
description="This block searches all public Tweets in Twitter history.",
categories={BlockCategory.SOCIAL},
input_schema=TwitterSearchRecentTweetsBlock.Input,
output_schema=TwitterSearchRecentTweetsBlock.Output,
test_input={
"query": "from:twitterapi #twitterapi",
"credentials": TEST_CREDENTIALS_INPUT,
"max_results": 10,
"start_time": "",
"end_time": "",
"since_id": "",
"until_id": "",
"sort_order": "",
"pagination": "",
"expansions": [],
"media_fields": [],
"place_fields": [],
"poll_fields": [],
"tweet_fields": [],
"user_fields": []
},
test_credentials=TEST_CREDENTIALS,
test_output=[
("tweet_ids", ["1373001119480344583", "1372627771717869568"]),
("tweet_texts", ["Looking to get started with the Twitter API but new to APIs in general?",
"Thanks to everyone who joined and made today a great session!"]),
("data", [
{
"id": "1373001119480344583",
"text": "Looking to get started with the Twitter API but new to APIs in general?"
},
{
"id": "1372627771717869568",
"text": "Thanks to everyone who joined and made today a great session!"
}
]),
("included", {}),
("meta", {
"newest_id": "1373001119480344583",
"oldest_id": "1372627771717869568",
"next_token": "next_token_value"
}),
("next_token", "next_token_value")
],
test_mock={
"search_tweets": lambda *args, **kwargs: (
["1373001119480344583", "1372627771717869568"],
["Looking to get started with the Twitter API but new to APIs in general?",
"Thanks to everyone who joined and made today a great session!"],
[
{
"id": "1373001119480344583",
"text": "Looking to get started with the Twitter API but new to APIs in general?"
},
{
"id": "1372627771717869568",
"text": "Thanks to everyone who joined and made today a great session!"
}
],
{},
{
"newest_id": "1373001119480344583",
"oldest_id": "1372627771717869568",
"next_token": "next_token_value"
},
"next_token_value"
)
},
)
@staticmethod
def search_tweets(
credentials: TwitterCredentials,
query: str,
max_results: int,
start_time: str,
end_time: str,
since_id: str,
until_id: str,
sort_order: str,
pagination: str,
expansions: list[TweetExpansions],
media_fields: list[TweetMediaFields],
place_fields: list[TweetPlaceFields],
poll_fields: list[TweetPollFields],
tweet_fields: list[TweetFields],
user_fields: list[TweetUserFields]
):
try:
client = tweepy.Client(
bearer_token=credentials.access_token.get_secret_value()
)
# Building common params
params = (TweetSearchBuilder()
.add_query(query)
.add_pagination(max_results, pagination)
.build())
# Adding expansions to params If required by the user
params = (TweetExpansionsBuilder(params)
.add_expansions(expansions)
.add_media_fields(media_fields)
.add_place_fields(place_fields)
.add_poll_fields(poll_fields)
.add_tweet_fields(tweet_fields)
.add_user_fields(user_fields)
.build())
# Adding time window to params If required by the user
params = (TweetDurationBuilder(params)
.add_start_time(start_time)
.add_end_time(end_time)
.add_since_id(since_id)
.add_until_id(until_id)
.add_sort_order(sort_order)
.build())
response = cast(
Response,
client.search_recent_tweets(**params)
)
if not response.data and not response.meta:
raise Exception("No tweets found")
meta = {}
tweet_ids = []
tweet_texts = []
next_token = None
if response.meta:
meta = response.meta
next_token = meta.get("next_token")
included = IncludesSerializer.serialize(response.includes)
data = ResponseDataSerializer.serialize_list(response.data)
if response.data:
tweet_ids = [str(tweet.id) for tweet in response.data]
tweet_texts = [tweet.text for tweet in response.data]
return tweet_ids, tweet_texts, data, included, meta, next_token
raise Exception("No tweets found")
except tweepy.TweepyException:
raise
def run(
self,
input_data: Input,
*,
credentials: TwitterCredentials,
**kwargs,
) -> BlockOutput:
try:
ids, texts, data, included, meta, next_token = self.search_tweets(
credentials,
input_data.query,
input_data.max_results,
input_data.start_time,
input_data.end_time,
input_data.since_id,
input_data.until_id,
input_data.sort_order,
input_data.pagination,
input_data.expansions,
input_data.media_fields,
input_data.place_fields,
input_data.poll_fields,
input_data.tweet_fields,
input_data.user_fields
)
if ids:
yield "tweet_ids", ids
if texts:
yield "tweet_texts", texts
if next_token:
yield "next_token", next_token
if data:
yield "data", data
if included:
yield "included", included
if meta:
yield "meta", meta
except Exception as e:
yield "error", handle_tweepy_exception(e)

View File

@ -0,0 +1,232 @@
from typing import cast
from backend.blocks.twitter._serializer import IncludesSerializer, ResponseDataSerializer
import tweepy
from tweepy.client import Response
from backend.data.block import Block, BlockCategory, BlockOutput, BlockSchema
from backend.data.model import SchemaField
from backend.blocks.twitter._builders import TweetExpansionsBuilder
from backend.blocks.twitter._types import TweetExcludes, TweetExpansionInputs,TweetExpansions, TweetFields, TweetMediaFields, TweetPlaceFields, TweetPollFields, TweetUserFields
from backend.blocks.twitter.tweepy_exceptions import handle_tweepy_exception
from backend.blocks.twitter._auth import (
TEST_CREDENTIALS,
TEST_CREDENTIALS_INPUT,
TwitterCredentials,
TwitterCredentialsField,
TwitterCredentialsInput,
)
class TwitterGetQuoteTweetsBlock(Block):
"""
Gets quote tweets for a specified tweet ID
"""
class Input(TweetExpansionInputs):
credentials: TwitterCredentialsInput = TwitterCredentialsField(
["tweet.read", "users.read", "offline.access"]
)
tweet_id: str = SchemaField(
description="ID of the tweet to get quotes for",
placeholder="Enter tweet ID",
)
max_results: int = SchemaField(
description="Number of results to return (max 100)",
default=10,
required=False,
advanced=True,
)
exclude: list[TweetExcludes] = SchemaField(
description="Types of tweets to exclude",
required=False,
advanced=True,
is_multi_select=True,
default=[],
)
pagination_token: str = SchemaField(
description="Token for pagination",
required=False,
advanced=True,
default="",
)
class Output(BlockSchema):
# Common Outputs that user commonly uses
ids : list = SchemaField(description="All Tweet IDs ")
texts : list = SchemaField(description="All Tweet texts")
next_token : str = SchemaField(description="Next token for pagination")
# Complete Outputs for advanced use
data : list[dict] = SchemaField(description="Complete Tweet data")
included : dict = SchemaField(description="Additional data that you have requested (Optional) via Expansions field")
meta : dict = SchemaField(description="Provides metadata such as pagination info (next_token) or result counts")
# error
error: str = SchemaField(description="Error message if the request failed")
def __init__(self):
super().__init__(
id="9fbdd208-a630-11ef-9b97-ab7a3a695ca3",
description="This block gets quote tweets for a specific tweet.",
categories={BlockCategory.SOCIAL},
input_schema=TwitterGetQuoteTweetsBlock.Input,
output_schema=TwitterGetQuoteTweetsBlock.Output,
test_input={
"tweet_id": "1234567890",
"max_results": 10,
"exclude": [],
"pagination_token": "",
"credentials": TEST_CREDENTIALS_INPUT,
"expansions": [],
"media_fields": [],
"place_fields": [],
"poll_fields": [],
"tweet_fields": [],
"user_fields": []
},
test_credentials=TEST_CREDENTIALS,
test_output=[
("ids", ["12345", "67890"]),
("texts", ["Tweet 1", "Tweet 2"]),
("data", [
{
"id": "12345",
"text": "Tweet 1"
},
{
"id": "67890",
"text": "Tweet 2"
}
]),
("included", {}),
("meta", {
"result_count": 2,
"next_token": "next_token_value"
}),
("next_token", "next_token_value")
],
test_mock={
"get_quote_tweets": lambda *args, **kwargs: (
["12345", "67890"],
["Tweet 1", "Tweet 2"],
[
{
"id": "12345",
"text": "Tweet 1"
},
{
"id": "67890",
"text": "Tweet 2"
}
],
{},
{
"result_count": 2,
"next_token": "next_token_value"
},
"next_token_value"
)
}
)
@staticmethod
def get_quote_tweets(
credentials: TwitterCredentials,
tweet_id: str,
max_results: int,
exclude: list[TweetExcludes] ,
pagination_token: str ,
expansions: list[TweetExpansions],
media_fields: list[TweetMediaFields],
place_fields: list[TweetPlaceFields],
poll_fields: list[TweetPollFields],
tweet_fields: list[TweetFields],
user_fields: list[TweetUserFields]
):
try:
client = tweepy.Client(
bearer_token=credentials.access_token.get_secret_value()
)
params = {"id": tweet_id,"max_results": max_results,
"pagination_token":None if pagination_token == "" else pagination_token,
"exclude": None if exclude == [] else exclude,
"user_auth": False}
params = (TweetExpansionsBuilder(params)
.add_expansions(expansions)
.add_media_fields(media_fields)
.add_place_fields(place_fields)
.add_poll_fields(poll_fields)
.add_tweet_fields(tweet_fields)
.add_user_fields(user_fields)
.build())
response = cast(
Response,
client.get_quote_tweets(**params)
)
meta = {}
tweet_ids = []
tweet_texts = []
next_token = None
if response.meta:
meta = response.meta
next_token = meta.get("next_token")
included = IncludesSerializer.serialize(response.includes)
data = ResponseDataSerializer.serialize_list(response.data)
if response.data:
tweet_ids = [str(tweet.id) for tweet in response.data]
tweet_texts = [tweet.text for tweet in response.data]
return tweet_ids, tweet_texts, data, included, meta, next_token
raise Exception("No quote tweets found")
except tweepy.TweepyException:
raise
def run(
self,
input_data: Input,
*,
credentials: TwitterCredentials,
**kwargs,
) -> BlockOutput:
try:
ids, texts, data, included, meta, next_token = self.get_quote_tweets(
credentials,
input_data.tweet_id,
input_data.max_results,
input_data.exclude,
input_data.pagination_token,
input_data.expansions,
input_data.media_fields,
input_data.place_fields,
input_data.poll_fields,
input_data.tweet_fields,
input_data.user_fields
)
if ids:
yield "ids", ids
if texts:
yield "texts", texts
if next_token:
yield "next_token", next_token
if data:
yield "data", data
if included:
yield "included", included
if meta:
yield "meta", meta
except Exception as e:
yield "error", handle_tweepy_exception(e)

View File

@ -0,0 +1,347 @@
from typing import cast
from backend.blocks.twitter._serializer import IncludesSerializer, ResponseDataSerializer
import tweepy
from tweepy.client import Response
from backend.data.block import Block, BlockCategory, BlockOutput, BlockSchema
from backend.data.model import SchemaField
from backend.blocks.twitter._builders import UserExpansionsBuilder
from backend.blocks.twitter._types import TweetFields, TweetUserFields, UserExpansionInputs, UserExpansions
from backend.blocks.twitter.tweepy_exceptions import handle_tweepy_exception
from backend.blocks.twitter._auth import (
TEST_CREDENTIALS,
TEST_CREDENTIALS_INPUT,
TwitterCredentials,
TwitterCredentialsField,
TwitterCredentialsInput,
)
class TwitterRetweetBlock(Block):
"""
Retweets a tweet on Twitter
"""
class Input(BlockSchema):
credentials: TwitterCredentialsInput = TwitterCredentialsField(
["tweet.read", "tweet.write", "users.read", "offline.access"]
)
tweet_id: str = SchemaField(
description="ID of the tweet to retweet",
placeholder="Enter tweet ID",
)
class Output(BlockSchema):
success : bool = SchemaField(description="Whether the retweet was successful")
error: str = SchemaField(description="Error message if the retweet failed")
def __init__(self):
super().__init__(
id="bd7b8d3a-a630-11ef-be96-6f4aa4c3c4f4",
description="This block retweets a tweet on Twitter.",
categories={BlockCategory.SOCIAL},
input_schema=TwitterRetweetBlock.Input,
output_schema=TwitterRetweetBlock.Output,
test_input={
"tweet_id": "1234567890",
"credentials": TEST_CREDENTIALS_INPUT,
},
test_credentials=TEST_CREDENTIALS,
test_output=[
("success", True),
("error", "")
],
)
@staticmethod
def retweet(
credentials: TwitterCredentials,
tweet_id: str,
):
try:
client = tweepy.Client(
bearer_token=credentials.access_token.get_secret_value()
)
client.retweet(
tweet_id=tweet_id,
user_auth=False,
)
return True
except tweepy.TweepyException:
raise
def run(
self,
input_data: Input,
*,
credentials: TwitterCredentials,
**kwargs,
) -> BlockOutput:
try:
success = self.retweet(
credentials,
input_data.tweet_id,
)
yield "success", success
except Exception as e:
yield "error", handle_tweepy_exception(e)
class TwitterRemoveRetweetBlock(Block):
"""
Removes a retweet on Twitter
"""
class Input(BlockSchema):
credentials: TwitterCredentialsInput = TwitterCredentialsField(
["tweet.read", "tweet.write", "users.read", "offline.access"]
)
tweet_id: str = SchemaField(
description="ID of the tweet to remove retweet",
placeholder="Enter tweet ID",
)
class Output(BlockSchema):
success: bool = SchemaField(
description="Whether the retweet was successfully removed"
)
error: str = SchemaField(description="Error message if the removal failed")
def __init__(self):
super().__init__(
id="b6e663f0-a630-11ef-a7f0-8b9b0c542ff8",
description="This block removes a retweet on Twitter.",
categories={BlockCategory.SOCIAL},
input_schema=TwitterRemoveRetweetBlock.Input,
output_schema=TwitterRemoveRetweetBlock.Output,
test_input={
"tweet_id": "1234567890",
"credentials": TEST_CREDENTIALS_INPUT,
},
test_credentials=TEST_CREDENTIALS,
test_output=[
("success", True),
],
)
@staticmethod
def remove_retweet(
credentials: TwitterCredentials,
tweet_id: str,
):
try:
client = tweepy.Client(
bearer_token=credentials.access_token.get_secret_value()
)
client.unretweet(
source_tweet_id=tweet_id,
user_auth=False,
)
return True
except tweepy.TweepyException:
raise
def run(
self,
input_data: Input,
*,
credentials: TwitterCredentials,
**kwargs,
) -> BlockOutput:
try:
success = self.remove_retweet(
credentials,
input_data.tweet_id,
)
yield "success", success
except Exception as e:
yield "error", handle_tweepy_exception(e)
class TwitterGetRetweetersBlock(Block):
"""
Gets information about who has retweeted a tweet
"""
class Input(UserExpansionInputs):
credentials: TwitterCredentialsInput = TwitterCredentialsField(
["tweet.read", "users.read", "offline.access"]
)
tweet_id: str = SchemaField(
description="ID of the tweet to get retweeters for",
placeholder="Enter tweet ID",
)
max_results: int = SchemaField(
description="Maximum number of results per page (1-100)",
default=10,
placeholder="Enter max results",
advanced=True,
)
pagination_token: str = SchemaField(
description="Token for pagination",
placeholder="Enter pagination token",
default="",
)
class Output(BlockSchema):
# Common Outputs that user commonly uses
ids: list = SchemaField(description="List of user ids who retweeted")
names: list = SchemaField(description="List of user names who retweeted")
usernames: list = SchemaField(description="List of user usernames who retweeted")
next_token: str = SchemaField(description="Token for next page of results")
# Complete Outputs for advanced use
data : list[dict] = SchemaField(description="Complete Tweet data")
included : dict = SchemaField(description="Additional data that you have requested (Optional) via Expansions field")
meta : dict = SchemaField(description="Provides metadata such as pagination info (next_token) or result counts")
error: str = SchemaField(description="Error message if the request failed")
def __init__(self):
super().__init__(
id="ad7aa6fa-a630-11ef-a6b0-e7ca640aa030",
description="This block gets information about who has retweeted a tweet.",
categories={BlockCategory.SOCIAL},
input_schema=TwitterGetRetweetersBlock.Input,
output_schema=TwitterGetRetweetersBlock.Output,
test_input={
"tweet_id": "1234567890",
"credentials": TEST_CREDENTIALS_INPUT,
"max_results": 1,
"pagination_token": "",
"expansions": [],
"media_fields": [],
"place_fields": [],
"poll_fields": [],
"tweet_fields": [],
"user_fields": []
},
test_credentials=TEST_CREDENTIALS,
test_output=[
("ids", ["12345"]),
("names", ["Test User"]),
("usernames", ["testuser"]),
("next_token", "next_token_value"),
("data", [{"id": "12345", "name": "Test User", "username": "testuser"}]),
("included", {}),
("meta", {"next_token": "next_token_value"}),
],
test_mock={
"get_retweeters": lambda *args, **kwargs: (
[{"id": "12345", "name": "Test User", "username": "testuser"}],
{},
{"next_token": "next_token_value"},
["12345"],
["Test User"],
["testuser"],
"next_token_value"
)
}
)
@staticmethod
def get_retweeters(
credentials: TwitterCredentials,
tweet_id: str,
max_results: int,
pagination_token: str ,
expansions: list[UserExpansions],
tweet_fields: list[TweetFields],
user_fields: list[TweetUserFields]
):
try:
client = tweepy.Client(
bearer_token=credentials.access_token.get_secret_value()
)
params = {
"id": tweet_id,
"max_results": max_results,
"pagination_token":None if pagination_token == "" else pagination_token,
"user_auth": False
}
params = (UserExpansionsBuilder(params)
.add_expansions(expansions)
.add_tweet_fields(tweet_fields)
.add_user_fields(user_fields)
.build())
response = cast(Response, client.get_retweeters(**params))
meta = {}
ids = []
names = []
usernames = []
next_token = None
if response.meta:
meta = response.meta
next_token = meta.get("next_token")
included = IncludesSerializer.serialize(response.includes)
data = ResponseDataSerializer.serialize_list(response.data)
if response.data:
ids = [str(user.id) for user in response.data]
names = [user.name for user in response.data]
usernames = [user.username for user in response.data]
return data, included, meta, ids, names, usernames, next_token
raise Exception("No retweeters found")
except tweepy.TweepyException:
raise
def run(
self,
input_data: Input,
*,
credentials: TwitterCredentials,
**kwargs,
) -> BlockOutput:
try:
data, included, meta, ids, names, usernames, next_token = self.get_retweeters(
credentials,
input_data.tweet_id,
input_data.max_results,
input_data.pagination_token,
input_data.expansions,
input_data.tweet_fields,
input_data.user_fields
)
if ids:
yield "ids", ids
if names:
yield "names", names
if usernames:
yield "usernames", usernames
if next_token:
yield "next_token", next_token
if data:
yield "data", data
if included:
yield "included", included
if meta:
yield "meta", meta
except Exception as e:
yield "error", handle_tweepy_exception(e)

View File

@ -0,0 +1,760 @@
from typing import cast
from backend.blocks.twitter._serializer import IncludesSerializer, ResponseDataSerializer
import tweepy
from tweepy.client import Response
from backend.data.block import Block, BlockCategory, BlockOutput, BlockSchema
from backend.data.model import SchemaField
from backend.blocks.twitter._builders import TweetDurationBuilder, TweetExpansionsBuilder
from backend.blocks.twitter._types import TweetExpansionInputs, TweetExpansions, TweetFields, TweetMediaFields, TweetPlaceFields, TweetPollFields, TweetTimeWindowInputs, TweetUserFields
from backend.blocks.twitter.tweepy_exceptions import handle_tweepy_exception
from backend.blocks.twitter._auth import (
TEST_CREDENTIALS,
TEST_CREDENTIALS_INPUT,
TwitterCredentials,
TwitterCredentialsField,
TwitterCredentialsInput,
)
class TwitterGetUserMentionsBlock(Block):
"""
Returns Tweets where a single user is mentioned, just put that user id
"""
class Input(TweetExpansionInputs,TweetTimeWindowInputs):
credentials: TwitterCredentialsInput = TwitterCredentialsField(
["tweet.read", "users.read", "offline.access"]
)
user_id: str = SchemaField(
description="Unique identifier of the user for whom to return Tweets mentioning the user",
placeholder="Enter user ID"
)
max_results: int = SchemaField(
description="Number of tweets to retrieve (5-100)",
default=10,
advanced=True
)
pagination_token: str = SchemaField(
description="Token for pagination",
default="",
advanced=True
)
class Output(BlockSchema):
# Common Outputs that user commonly uses
ids : list[str] = SchemaField(description="List of Tweet IDs")
texts : list[str] = SchemaField(description="All Tweet texts")
userIds: list[str] = SchemaField(description="List of user ids that mentioned the user")
userNames: list[str] = SchemaField(description="List of user names that mentioned the user")
next_token : str = SchemaField(description="Next token for pagination")
# Complete Outputs for advanced use
data : list[dict] = SchemaField(description="Complete Tweet data")
included : dict = SchemaField(description="Additional data that you have requested (Optional) via Expansions field")
meta : dict = SchemaField(description="Provides metadata such as pagination info (next_token) or result counts")
# error
error: str = SchemaField(description="Error message if the request failed")
def __init__(self):
super().__init__(
id="e01c890c-a630-11ef-9e20-37da24888bd0",
description="This block retrieves Tweets mentioning a specific user.",
categories={BlockCategory.SOCIAL},
input_schema=TwitterGetUserMentionsBlock.Input,
output_schema=TwitterGetUserMentionsBlock.Output,
test_input={
"user_id": "12345",
"credentials": TEST_CREDENTIALS_INPUT,
"max_results": 10,
"start_time": "",
"end_time": "",
"since_id": "",
"until_id": "",
"sort_order": "",
"pagination": "",
"expansions": [],
"media_fields": [],
"place_fields": [],
"poll_fields": [],
"tweet_fields": [],
"user_fields": []
},
test_credentials=TEST_CREDENTIALS,
test_output=[
("ids", ["1373001119480344583", "1372627771717869568"]),
("texts", ["Test mention 1", "Test mention 2"]),
("userIds", ["67890", "67891"]),
("userNames", ["testuser1", "testuser2"]),
("data", [
{
"id": "1373001119480344583",
"text": "Test mention 1"
},
{
"id": "1372627771717869568",
"text": "Test mention 2"
}
]),
("included", {
"users": [
{"id": "67890", "username": "testuser1"},
{"id": "67891", "username": "testuser2"}
]
}),
("meta", {
"newest_id": "1373001119480344583",
"oldest_id": "1372627771717869568",
"next_token": "next_token_value"
}),
("next_token", "next_token_value")
],
test_mock={
"get_mentions": lambda *args, **kwargs: (
["1373001119480344583", "1372627771717869568"],
["Test mention 1", "Test mention 2"],
["67890", "67891"],
["testuser1", "testuser2"],
[
{"id": "1373001119480344583", "text": "Test mention 1"},
{"id": "1372627771717869568", "text": "Test mention 2"}
],
{
"users": [
{"id": "67890", "username": "testuser1"},
{"id": "67891", "username": "testuser2"}
]
},
{
"newest_id": "1373001119480344583",
"oldest_id": "1372627771717869568",
"next_token": "next_token_value"
},
"next_token_value"
)
}
)
@staticmethod
def get_mentions(
credentials: TwitterCredentials,
user_id: str,
max_results: int,
start_time: str,
end_time: str,
since_id: str,
until_id: str,
sort_order: str,
pagination: str,
expansions: list[TweetExpansions],
media_fields: list[TweetMediaFields],
place_fields: list[TweetPlaceFields],
poll_fields: list[TweetPollFields],
tweet_fields: list[TweetFields],
user_fields: list[TweetUserFields]
):
try:
client = tweepy.Client(
bearer_token=credentials.access_token.get_secret_value()
)
params = {
"id": user_id,
"max_results": max_results,
"pagination_token": None if pagination == "" else pagination,
"user_auth": False
}
# Adding expansions to params If required by the user
params = (TweetExpansionsBuilder(params)
.add_expansions(expansions)
.add_media_fields(media_fields)
.add_place_fields(place_fields)
.add_poll_fields(poll_fields)
.add_tweet_fields(tweet_fields)
.add_user_fields(user_fields)
.build())
# Adding time window to params If required by the user
params = (TweetDurationBuilder(params)
.add_start_time(start_time)
.add_end_time(end_time)
.add_since_id(since_id)
.add_until_id(until_id)
.add_sort_order(sort_order)
.build())
response = cast(
Response,
client.get_users_mentions(
**params
),
)
if not response.data and not response.meta:
raise Exception("No tweets found")
included = IncludesSerializer.serialize(response.includes)
data = ResponseDataSerializer.serialize_list(response.data)
meta = response.meta or {}
next_token = meta.get("next_token", "")
tweet_ids = []
tweet_texts = []
user_ids = []
user_names = []
if response.data:
tweet_ids = [str(tweet.id) for tweet in response.data]
tweet_texts = [tweet.text for tweet in response.data]
if "users" in included:
user_ids = [str(user["id"]) for user in included["users"]]
user_names = [user["username"] for user in included["users"]]
return tweet_ids, tweet_texts, user_ids, user_names, data, included, meta, next_token
except tweepy.TweepyException:
raise
def run(
self,
input_data: Input,
*,
credentials: TwitterCredentials,
**kwargs,
) -> BlockOutput:
try:
ids, texts, user_ids, user_names, data, included, meta, next_token = self.get_mentions(
credentials,
input_data.user_id,
input_data.max_results,
input_data.start_time,
input_data.end_time,
input_data.since_id,
input_data.until_id,
input_data.sort_order,
input_data.pagination_token,
input_data.expansions,
input_data.media_fields,
input_data.place_fields,
input_data.poll_fields,
input_data.tweet_fields,
input_data.user_fields
)
if ids:
yield "ids", ids
if texts:
yield "texts", texts
if user_ids:
yield "userIds", user_ids
if user_names:
yield "userNames", user_names
if next_token:
yield "next_token", next_token
if data:
yield "data", data
if included:
yield "included", included
if meta:
yield "meta", meta
except Exception as e:
yield "error", handle_tweepy_exception(e)
class TwitterGetHomeTimelineBlock(Block):
"""
Returns a collection of the most recent Tweets and Retweets posted by you and users you follow
"""
class Input(TweetExpansionInputs,TweetTimeWindowInputs):
credentials: TwitterCredentialsInput = TwitterCredentialsField(
["tweet.read", "users.read", "offline.access"]
)
max_results: int = SchemaField(
description="Number of tweets to retrieve (5-100)",
default=10,
advanced=True
)
pagination_token: str = SchemaField(
description="Token for pagination",
default="",
advanced=True
)
class Output(BlockSchema):
# Common Outputs that user commonly uses
ids : list[str] = SchemaField(description="List of Tweet IDs")
texts : list[str] = SchemaField(description="All Tweet texts")
userIds: list[str] = SchemaField(description="List of user ids that authored the tweets")
userNames: list[str] = SchemaField(description="List of user names that authored the tweets")
next_token : str = SchemaField(description="Next token for pagination")
# Complete Outputs for advanced use
data : list[dict] = SchemaField(description="Complete Tweet data")
included : dict = SchemaField(description="Additional data that you have requested (Optional) via Expansions field")
meta : dict = SchemaField(description="Provides metadata such as pagination info (next_token) or result counts")
# error
error: str = SchemaField(description="Error message if the request failed")
def __init__(self):
super().__init__(
id="d222a070-a630-11ef-a18a-3f52f76c6962",
description="This block retrieves the authenticated user's home timeline.",
categories={BlockCategory.SOCIAL},
input_schema=TwitterGetHomeTimelineBlock.Input,
output_schema=TwitterGetHomeTimelineBlock.Output,
test_input={
"credentials": TEST_CREDENTIALS_INPUT,
"max_results": 10,
"start_time": "",
"end_time": "",
"since_id": "",
"until_id": "",
"sort_order": "",
"pagination": "",
"expansions": [],
"media_fields": [],
"place_fields": [],
"poll_fields": [],
"tweet_fields": [],
"user_fields": []
},
test_credentials=TEST_CREDENTIALS,
test_output=[
("ids", ["1373001119480344583", "1372627771717869568"]),
("texts", ["Test tweet 1", "Test tweet 2"]),
("userIds", ["67890", "67891"]),
("userNames", ["testuser1", "testuser2"]),
("data", [
{
"id": "1373001119480344583",
"text": "Test tweet 1"
},
{
"id": "1372627771717869568",
"text": "Test tweet 2"
}
]),
("included", {
"users": [
{"id": "67890", "username": "testuser1"},
{"id": "67891", "username": "testuser2"}
]
}),
("meta", {
"newest_id": "1373001119480344583",
"oldest_id": "1372627771717869568",
"next_token": "next_token_value"
}),
("next_token", "next_token_value")
],
test_mock={
"get_timeline": lambda *args, **kwargs: (
["1373001119480344583", "1372627771717869568"],
["Test tweet 1", "Test tweet 2"],
["67890", "67891"],
["testuser1", "testuser2"],
[
{"id": "1373001119480344583", "text": "Test tweet 1"},
{"id": "1372627771717869568", "text": "Test tweet 2"}
],
{
"users": [
{"id": "67890", "username": "testuser1"},
{"id": "67891", "username": "testuser2"}
]
},
{
"newest_id": "1373001119480344583",
"oldest_id": "1372627771717869568",
"next_token": "next_token_value"
},
"next_token_value"
)
}
)
@staticmethod
def get_timeline(
credentials: TwitterCredentials,
max_results: int,
start_time: str,
end_time: str,
since_id: str,
until_id: str,
sort_order: str,
pagination: str,
expansions: list[TweetExpansions],
media_fields: list[TweetMediaFields],
place_fields: list[TweetPlaceFields],
poll_fields: list[TweetPollFields],
tweet_fields: list[TweetFields],
user_fields: list[TweetUserFields]
):
try:
client = tweepy.Client(
bearer_token=credentials.access_token.get_secret_value()
)
params = {
"max_results": max_results,
"pagination_token": None if pagination == "" else pagination,
"user_auth": False
}
# Adding expansions to params If required by the user
params = (TweetExpansionsBuilder(params)
.add_expansions(expansions)
.add_media_fields(media_fields)
.add_place_fields(place_fields)
.add_poll_fields(poll_fields)
.add_tweet_fields(tweet_fields)
.add_user_fields(user_fields)
.build())
# Adding time window to params If required by the user
params = (TweetDurationBuilder(params)
.add_start_time(start_time)
.add_end_time(end_time)
.add_since_id(since_id)
.add_until_id(until_id)
.add_sort_order(sort_order)
.build())
response = cast(
Response,
client.get_home_timeline(
**params
),
)
if not response.data and not response.meta:
raise Exception("No tweets found")
included = IncludesSerializer.serialize(response.includes)
data = ResponseDataSerializer.serialize_list(response.data)
meta = response.meta or {}
next_token = meta.get("next_token", "")
tweet_ids = []
tweet_texts = []
user_ids = []
user_names = []
if response.data:
tweet_ids = [str(tweet.id) for tweet in response.data]
tweet_texts = [tweet.text for tweet in response.data]
if "users" in included:
user_ids = [str(user["id"]) for user in included["users"]]
user_names = [user["username"] for user in included["users"]]
return tweet_ids, tweet_texts, user_ids, user_names, data, included, meta, next_token
except tweepy.TweepyException:
raise
def run(
self,
input_data: Input,
*,
credentials: TwitterCredentials,
**kwargs,
) -> BlockOutput:
try:
ids, texts, user_ids, user_names, data, included, meta, next_token = self.get_timeline(
credentials,
input_data.max_results,
input_data.start_time,
input_data.end_time,
input_data.since_id,
input_data.until_id,
input_data.sort_order,
input_data.pagination_token,
input_data.expansions,
input_data.media_fields,
input_data.place_fields,
input_data.poll_fields,
input_data.tweet_fields,
input_data.user_fields
)
if ids:
yield "ids", ids
if texts:
yield "texts", texts
if user_ids:
yield "userIds", user_ids
if user_names:
yield "userNames", user_names
if next_token:
yield "next_token", next_token
if data:
yield "data", data
if included:
yield "included", included
if meta:
yield "meta", meta
except Exception as e:
yield "error", handle_tweepy_exception(e)
class TwitterGetUserTweetsBlock(Block):
"""
Returns Tweets composed by a single user, specified by the requested user ID
"""
class Input(TweetExpansionInputs,TweetTimeWindowInputs):
credentials: TwitterCredentialsInput = TwitterCredentialsField(
["tweet.read", "users.read", "offline.access"]
)
user_id: str = SchemaField(
description="Unique identifier of the Twitter account (user ID) for whom to return results",
placeholder="Enter user ID"
)
max_results: int = SchemaField(
description="Number of tweets to retrieve (5-100)",
default=10,
advanced=True
)
pagination_token: str = SchemaField(
description="Token for pagination",
default="",
advanced=True
)
class Output(BlockSchema):
# Common Outputs that user commonly uses
ids : list[str] = SchemaField(description="List of Tweet IDs")
texts : list[str] = SchemaField(description="All Tweet texts")
userIds: list[str] = SchemaField(description="List of user ids that authored the tweets")
userNames: list[str] = SchemaField(description="List of user names that authored the tweets")
next_token : str = SchemaField(description="Next token for pagination")
# Complete Outputs for advanced use
data : list[dict] = SchemaField(description="Complete Tweet data")
included : dict = SchemaField(description="Additional data that you have requested (Optional) via Expansions field")
meta : dict = SchemaField(description="Provides metadata such as pagination info (next_token) or result counts")
# error
error: str = SchemaField(description="Error message if the request failed")
def __init__(self):
super().__init__(
id="c44c3ef2-a630-11ef-9ff7-eb7b5ea3a5cb",
description="This block retrieves Tweets composed by a single user.",
categories={BlockCategory.SOCIAL},
input_schema=TwitterGetUserTweetsBlock.Input,
output_schema=TwitterGetUserTweetsBlock.Output,
test_input={
"user_id": "12345",
"credentials": TEST_CREDENTIALS_INPUT,
"max_results": 10,
"start_time": "",
"end_time": "",
"since_id": "",
"until_id": "",
"sort_order": "",
"pagination": "",
"expansions": [],
"media_fields": [],
"place_fields": [],
"poll_fields": [],
"tweet_fields": [],
"user_fields": []
},
test_credentials=TEST_CREDENTIALS,
test_output=[
("ids", ["1373001119480344583", "1372627771717869568"]),
("texts", ["Test tweet 1", "Test tweet 2"]),
("userIds", ["67890", "67891"]),
("userNames", ["testuser1", "testuser2"]),
("data", [
{
"id": "1373001119480344583",
"text": "Test tweet 1"
},
{
"id": "1372627771717869568",
"text": "Test tweet 2"
}
]),
("included", {
"users": [
{"id": "67890", "username": "testuser1"},
{"id": "67891", "username": "testuser2"}
]
}),
("meta", {
"newest_id": "1373001119480344583",
"oldest_id": "1372627771717869568",
"next_token": "next_token_value"
}),
("next_token", "next_token_value")
],
test_mock={
"get_user_tweets": lambda *args, **kwargs: (
["1373001119480344583", "1372627771717869568"],
["Test tweet 1", "Test tweet 2"],
["67890", "67891"],
["testuser1", "testuser2"],
[
{"id": "1373001119480344583", "text": "Test tweet 1"},
{"id": "1372627771717869568", "text": "Test tweet 2"}
],
{
"users": [
{"id": "67890", "username": "testuser1"},
{"id": "67891", "username": "testuser2"}
]
},
{
"newest_id": "1373001119480344583",
"oldest_id": "1372627771717869568",
"next_token": "next_token_value"
},
"next_token_value"
)
}
)
@staticmethod
def get_user_tweets(
credentials: TwitterCredentials,
user_id: str,
max_results: int,
start_time: str,
end_time: str,
since_id: str,
until_id: str,
sort_order: str,
pagination: str,
expansions: list[TweetExpansions],
media_fields: list[TweetMediaFields],
place_fields: list[TweetPlaceFields],
poll_fields: list[TweetPollFields],
tweet_fields: list[TweetFields],
user_fields: list[TweetUserFields]
):
try:
client = tweepy.Client(
bearer_token=credentials.access_token.get_secret_value()
)
params = {
"id": user_id,
"max_results": max_results,
"pagination_token": None if pagination == "" else pagination,
"user_auth": False
}
# Adding expansions to params If required by the user
params = (TweetExpansionsBuilder(params)
.add_expansions(expansions)
.add_media_fields(media_fields)
.add_place_fields(place_fields)
.add_poll_fields(poll_fields)
.add_tweet_fields(tweet_fields)
.add_user_fields(user_fields)
.build())
# Adding time window to params If required by the user
params = (TweetDurationBuilder(params)
.add_start_time(start_time)
.add_end_time(end_time)
.add_since_id(since_id)
.add_until_id(until_id)
.add_sort_order(sort_order)
.build())
response = cast(
Response,
client.get_users_tweets(
**params
),
)
if not response.data and not response.meta:
raise Exception("No tweets found")
included = IncludesSerializer.serialize(response.includes)
data = ResponseDataSerializer.serialize_list(response.data)
meta = response.meta or {}
next_token = meta.get("next_token", "")
tweet_ids = []
tweet_texts = []
user_ids = []
user_names = []
if response.data:
tweet_ids = [str(tweet.id) for tweet in response.data]
tweet_texts = [tweet.text for tweet in response.data]
if "users" in included:
user_ids = [str(user["id"]) for user in included["users"]]
user_names = [user["username"] for user in included["users"]]
return tweet_ids, tweet_texts, user_ids, user_names, data, included, meta, next_token
except tweepy.TweepyException:
raise
def run(
self,
input_data: Input,
*,
credentials: TwitterCredentials,
**kwargs,
) -> BlockOutput:
try:
ids, texts, user_ids, user_names, data, included, meta, next_token = self.get_user_tweets(
credentials,
input_data.user_id,
input_data.max_results,
input_data.start_time,
input_data.end_time,
input_data.since_id,
input_data.until_id,
input_data.sort_order,
input_data.pagination_token,
input_data.expansions,
input_data.media_fields,
input_data.place_fields,
input_data.poll_fields,
input_data.tweet_fields,
input_data.user_fields
)
if ids:
yield "ids", ids
if texts:
yield "texts", texts
if user_ids:
yield "userIds", user_ids
if user_names:
yield "userNames", user_names
if next_token:
yield "next_token", next_token
if data:
yield "data", data
if included:
yield "included", included
if meta:
yield "meta", meta
except Exception as e:
yield "error", handle_tweepy_exception(e)

View File

@ -0,0 +1,329 @@
from typing import cast
from backend.blocks.twitter._serializer import IncludesSerializer, ResponseDataSerializer
import tweepy
from tweepy.client import Response
from backend.data.block import Block, BlockCategory, BlockOutput, BlockSchema
from backend.data.model import SchemaField
from backend.blocks.twitter._types import TweetExpansions, TweetMediaFields, TweetPlaceFields, TweetPollFields, TweetFields, TweetUserFields, TweetExpansionInputs
from backend.blocks.twitter._builders import TweetExpansionsBuilder
from backend.blocks.twitter.tweepy_exceptions import handle_tweepy_exception
from backend.blocks.twitter._auth import (
TEST_CREDENTIALS,
TEST_CREDENTIALS_INPUT,
TwitterCredentials,
TwitterCredentialsField,
TwitterCredentialsInput,
)
class TwitterGetTweetBlock(Block):
"""
Returns information about a single Tweet specified by the requested ID
"""
class Input(TweetExpansionInputs):
credentials: TwitterCredentialsInput = TwitterCredentialsField(
["tweet.read", "users.read", "offline.access"]
)
tweet_id: str = SchemaField(
description="Unique identifier of the Tweet to request (ex: 1460323737035677698)",
placeholder="Enter tweet ID"
)
class Output(BlockSchema):
# Common Outputs that user commonly uses
id : str = SchemaField(description="Tweet ID")
text : str = SchemaField(description="Tweet text")
userId: str = SchemaField(description="ID of the tweet author")
userName: str = SchemaField(description="Username of the tweet author")
# Complete Outputs for advanced use
data: dict = SchemaField(description="Tweet data")
included: dict = SchemaField(description="Additional data that you have requested (Optional) via Expansions field")
meta: dict = SchemaField(description="Metadata about the tweet")
error: str = SchemaField(description="Error message if the request failed")
def __init__(self):
super().__init__(
id="f5155c3a-a630-11ef-9cc1-a309988b4d92",
description="This block retrieves information about a specific Tweet.",
categories={BlockCategory.SOCIAL},
input_schema=TwitterGetTweetBlock.Input,
output_schema=TwitterGetTweetBlock.Output,
test_input={
"tweet_id": "1460323737035677698",
"credentials": TEST_CREDENTIALS_INPUT,
"expansions": [],
"media_fields": [],
"place_fields": [],
"poll_fields": [],
"tweet_fields": [],
"user_fields": []
},
test_credentials=TEST_CREDENTIALS,
test_output=[
("id", "1460323737035677698"),
("text", "Test tweet content"),
("userId", "12345"),
("userName", "testuser"),
("data", {"id": "1460323737035677698", "text": "Test tweet content"}),
("included", {"users": [{"id": "12345", "username": "testuser"}]}),
("meta", {"result_count": 1}),
],
test_mock={
"get_tweet": lambda *args, **kwargs: ({
"id": "1460323737035677698",
"text": "Test tweet content"
}, {})
}
)
@staticmethod
def get_tweet(
credentials: TwitterCredentials,
tweet_id: str,
expansions: list[TweetExpansions],
media_fields: list[TweetMediaFields],
place_fields: list[TweetPlaceFields],
poll_fields: list[TweetPollFields],
tweet_fields: list[TweetFields],
user_fields: list[TweetUserFields]
):
try:
client = tweepy.Client(
bearer_token=credentials.access_token.get_secret_value()
)
params = {"id": tweet_id,"user_auth": False}
# Adding expansions to params If required by the user
params = (TweetExpansionsBuilder(params)
.add_expansions(expansions)
.add_media_fields(media_fields)
.add_place_fields(place_fields)
.add_poll_fields(poll_fields)
.add_tweet_fields(tweet_fields)
.add_user_fields(user_fields)
.build())
response = cast(Response, client.get_tweet(**params))
meta = {}
user_id = ""
user_name = ""
if response.meta:
meta = response.meta
included = IncludesSerializer.serialize(response.includes)
data = ResponseDataSerializer.serialize_dict(response.data)
if included and "users" in included:
user_id = str(included["users"][0]["id"])
user_name = included["users"][0]["username"]
if response.data:
return data, included, meta, user_id, user_name
raise Exception("Tweet not found")
except tweepy.TweepyException:
raise
def run(
self,
input_data: Input,
*,
credentials: TwitterCredentials,
**kwargs,
) -> BlockOutput:
try:
tweet_data, included, meta, user_id, user_name = self.get_tweet(
credentials,
input_data.tweet_id,
input_data.expansions,
input_data.media_fields,
input_data.place_fields,
input_data.poll_fields,
input_data.tweet_fields,
input_data.user_fields
)
yield "id", str(tweet_data["id"])
yield "text", tweet_data["text"]
if user_id:
yield "userId", user_id
if user_name:
yield "userName", user_name
yield "data", tweet_data
if included:
yield "included", included
if meta:
yield "meta", meta
except Exception as e:
yield "error", handle_tweepy_exception(e)
class TwitterGetTweetsBlock(Block):
"""
Returns information about multiple Tweets specified by the requested IDs
"""
class Input(TweetExpansionInputs):
credentials: TwitterCredentialsInput = TwitterCredentialsField(
["tweet.read", "users.read", "offline.access"]
)
tweet_ids: list[str] = SchemaField(
description="List of Tweet IDs to request (up to 100)",
placeholder="Enter tweet IDs"
)
class Output(BlockSchema):
# Common Outputs that user commonly uses
ids : list[str] = SchemaField(description="All Tweet IDs")
texts : list[str] = SchemaField(description="All Tweet texts")
userIds: list[str] = SchemaField(description="List of user ids that authored the tweets")
userNames: list[str] = SchemaField(description="List of user names that authored the tweets")
# Complete Outputs for advanced use
data: list[dict] = SchemaField(description="Complete Tweet data")
included: dict = SchemaField(description="Additional data that you have requested (Optional) via Expansions field")
meta: dict = SchemaField(description="Metadata about the tweets")
error: str = SchemaField(description="Error message if the request failed")
def __init__(self):
super().__init__(
id="e7cc5420-a630-11ef-bfaf-13bdd8096a51",
description="This block retrieves information about multiple Tweets.",
categories={BlockCategory.SOCIAL},
input_schema=TwitterGetTweetsBlock.Input,
output_schema=TwitterGetTweetsBlock.Output,
test_input={
"tweet_ids": ["1460323737035677698"],
"credentials": TEST_CREDENTIALS_INPUT,
"expansions": [],
"media_fields": [],
"place_fields": [],
"poll_fields": [],
"tweet_fields": [],
"user_fields": []
},
test_credentials=TEST_CREDENTIALS,
test_output=[
("ids", ["1460323737035677698"]),
("texts", ["Test tweet content"]),
("userIds", ["67890"]),
("userNames", ["testuser1"]),
("data", [{"id": "1460323737035677698", "text": "Test tweet content"}]),
("included", {"users": [{"id": "67890", "username": "testuser1"}]}),
("meta", {"result_count": 1})
],
test_mock={
"get_tweets": lambda *args, **kwargs: ({
"id": "1460323737035677698",
"text": "Test tweet content"
}, {})
}
)
@staticmethod
def get_tweets(
credentials: TwitterCredentials,
tweet_ids: list[str],
expansions: list[TweetExpansions],
media_fields: list[TweetMediaFields],
place_fields: list[TweetPlaceFields],
poll_fields: list[TweetPollFields],
tweet_fields: list[TweetFields],
user_fields: list[TweetUserFields]
):
try:
client = tweepy.Client(
bearer_token=credentials.access_token.get_secret_value()
)
params = {"ids": tweet_ids,"user_auth": False}
# Adding expansions to params If required by the user
params = (TweetExpansionsBuilder(params)
.add_expansions(expansions)
.add_media_fields(media_fields)
.add_place_fields(place_fields)
.add_poll_fields(poll_fields)
.add_tweet_fields(tweet_fields)
.add_user_fields(user_fields)
.build())
response = cast(Response, client.get_tweets(**params))
if not response.data and not response.meta:
raise Exception("No tweets found")
tweet_ids = []
tweet_texts = []
user_ids = []
user_names = []
meta = {}
included = IncludesSerializer.serialize(response.includes)
data = ResponseDataSerializer.serialize_list(response.data)
if response.data:
tweet_ids = [str(tweet.id) for tweet in response.data]
tweet_texts = [tweet.text for tweet in response.data]
if included and "users" in included:
for user in included["users"]:
user_ids.append(str(user["id"]))
user_names.append(user["username"])
if response.meta:
meta = response.meta
return tweet_ids, tweet_texts, user_ids, user_names, data, included, meta
except tweepy.TweepyException:
raise
def run(
self,
input_data: Input,
*,
credentials: TwitterCredentials,
**kwargs,
) -> BlockOutput:
try:
ids, texts, user_ids, user_names, data, included, meta = self.get_tweets(
credentials,
input_data.tweet_ids,
input_data.expansions,
input_data.media_fields,
input_data.place_fields,
input_data.poll_fields,
input_data.tweet_fields,
input_data.user_fields
)
if ids:
yield "ids", ids
if texts:
yield "texts", texts
if user_ids:
yield "userIds", user_ids
if user_names:
yield "userNames", user_names
if data:
yield "data", data
if included:
yield "included", included
if meta:
yield "meta", meta
except Exception as e:
yield "error", handle_tweepy_exception(e)

View File

@ -0,0 +1,304 @@
from typing import cast
from backend.blocks.twitter._serializer import IncludesSerializer
import tweepy
from tweepy.client import Response
from backend.blocks.twitter._types import TweetFields, TweetUserFields, UserExpansionInputs, UserExpansions
from backend.blocks.twitter.tweepy_exceptions import handle_tweepy_exception
from backend.data.block import Block, BlockCategory, BlockOutput, BlockSchema
from backend.data.model import SchemaField
from backend.blocks.twitter._builders import UserExpansionsBuilder
from backend.blocks.twitter._auth import (
TEST_CREDENTIALS,
TEST_CREDENTIALS_INPUT,
TwitterCredentials,
TwitterCredentialsField,
TwitterCredentialsInput,
)
class TwitterUnblockUserBlock(Block):
"""
Unblock a specific user on Twitter
"""
class Input(BlockSchema):
credentials: TwitterCredentialsInput = TwitterCredentialsField(
["block.write", "users.read", "offline.access"]
)
target_user_id: str = SchemaField(
description="The user ID of the user that you would like to unblock",
placeholder="Enter target user ID"
)
class Output(BlockSchema):
success: bool = SchemaField(description="Whether the unblock was successful")
error: str = SchemaField(description="Error message if the request failed")
def __init__(self):
super().__init__(
id="0f1b6570-a631-11ef-a3ea-230cbe9650dd",
description="This block unblocks a specific user on Twitter.",
categories={BlockCategory.SOCIAL},
input_schema=TwitterUnblockUserBlock.Input,
output_schema=TwitterUnblockUserBlock.Output,
test_input={
"target_user_id": "12345",
"credentials": TEST_CREDENTIALS_INPUT
},
test_credentials=TEST_CREDENTIALS,
test_output=[
("success", True),
],
)
@staticmethod
def unblock_user(
credentials: TwitterCredentials,
target_user_id: str
):
try:
client = tweepy.Client(
bearer_token=credentials.access_token.get_secret_value()
)
client.unblock(
target_user_id=target_user_id,
user_auth=False
)
return True
except tweepy.TweepyException:
raise
def run(
self,
input_data: Input,
*,
credentials: TwitterCredentials,
**kwargs,
) -> BlockOutput:
try:
success = self.unblock_user(
credentials,
input_data.target_user_id
)
yield "success", success
except Exception as e:
yield "error", handle_tweepy_exception(e)
class TwitterGetBlockedUsersBlock(Block):
"""
Get a list of users who are blocked by the authenticating user
"""
class Input(UserExpansionInputs):
credentials: TwitterCredentialsInput = TwitterCredentialsField(
["users.read", "offline.access", "block.read"]
)
max_results: int = SchemaField(
description="Maximum number of results to return (1-1000, default 100)",
placeholder="Enter max results",
default=10,
advanced=True
)
pagination_token: str = SchemaField(
description="Token for retrieving next/previous page of results",
placeholder="Enter pagination token",
default="",
advanced=True
)
class Output(BlockSchema):
user_ids: list[str] = SchemaField(description="List of blocked user IDs")
usernames_: list[str] = SchemaField(description="List of blocked usernames")
included: dict = SchemaField(description="Additional data requested via expansions")
meta: dict = SchemaField(description="Metadata including pagination info")
next_token: str = SchemaField(description="Next token for pagination")
error: str = SchemaField(description="Error message if the request failed")
def __init__(self):
super().__init__(
id="05f409e8-a631-11ef-ae89-93de863ee30d",
description="This block retrieves a list of users blocked by the authenticating user.",
categories={BlockCategory.SOCIAL},
input_schema=TwitterGetBlockedUsersBlock.Input,
output_schema=TwitterGetBlockedUsersBlock.Output,
test_input={
"max_results": 10,
"pagination_token": "",
"credentials": TEST_CREDENTIALS_INPUT,
"expansions": [],
"tweet_fields": [],
"user_fields": []
},
test_credentials=TEST_CREDENTIALS,
test_output=[
("user_ids", ["12345", "67890"]),
("usernames_", ["testuser1", "testuser2"]),
("included", {}),
("meta", {"next_token": "next_token_value"}),
("next_token", "next_token_value")
],
)
@staticmethod
def get_blocked_users(
credentials: TwitterCredentials,
max_results: int,
pagination_token: str,
expansions: list[UserExpansions],
tweet_fields: list[TweetFields],
user_fields: list[TweetUserFields],
):
try:
client = tweepy.Client(
bearer_token=credentials.access_token.get_secret_value()
)
params = {
"max_results": max_results,
"pagination_token": None if pagination_token == "" else pagination_token,
"user_auth": False
}
params = (UserExpansionsBuilder(params)
.add_expansions(expansions)
.add_tweet_fields(tweet_fields)
.add_user_fields(user_fields)
.build())
response = cast(
Response,
client.get_blocked(
**params
)
)
meta = {}
user_ids = []
usernames = []
next_token = None
included = IncludesSerializer.serialize(response.includes)
if response.data:
for user in response.data:
user_ids.append(str(user.id))
usernames.append(user.username)
if response.meta:
meta = response.meta
if "next_token" in meta:
next_token = meta["next_token"]
if user_ids and usernames:
return included, meta, user_ids, usernames, next_token
else:
raise tweepy.TweepyException("No blocked users found")
except tweepy.TweepyException:
raise
def run(
self,
input_data: Input,
*,
credentials: TwitterCredentials,
**kwargs,
) -> BlockOutput:
try:
included, meta, user_ids, usernames, next_token = self.get_blocked_users(
credentials,
input_data.max_results,
input_data.pagination_token,
input_data.expansions,
input_data.tweet_fields,
input_data.user_fields
)
if user_ids:
yield "user_ids", user_ids
if usernames:
yield "usernames_", usernames
if included:
yield "included", included
if meta:
yield "meta", meta
if next_token:
yield "next_token", next_token
except Exception as e:
yield "error", handle_tweepy_exception(e)
class TwitterBlockUserBlock(Block):
"""
Block a specific user on Twitter
"""
class Input(BlockSchema):
credentials: TwitterCredentialsInput = TwitterCredentialsField(
["block.write", "users.read", "offline.access"]
)
target_user_id: str = SchemaField(
description="The user ID of the user that you would like to block",
placeholder="Enter target user ID"
)
class Output(BlockSchema):
success: bool = SchemaField(description="Whether the block was successful")
error: str = SchemaField(description="Error message if the request failed")
def __init__(self):
super().__init__(
id="fc258b94-a630-11ef-abc3-df050b75b816",
description="This block blocks a specific user on Twitter.",
categories={BlockCategory.SOCIAL},
input_schema=TwitterBlockUserBlock.Input,
output_schema=TwitterBlockUserBlock.Output,
test_input={
"target_user_id": "12345",
"credentials": TEST_CREDENTIALS_INPUT
},
test_credentials=TEST_CREDENTIALS,
test_output=[
("success", True),
],
)
@staticmethod
def block_user(
credentials: TwitterCredentials,
target_user_id: str
):
try:
client = tweepy.Client(
bearer_token=credentials.access_token.get_secret_value()
)
client.block(target_user_id=target_user_id,user_auth=False)
return True
except tweepy.TweepyException:
raise
def run(
self,
input_data: Input,
*,
credentials: TwitterCredentials,
**kwargs,
) -> BlockOutput:
try:
success = self.block_user(
credentials,
input_data.target_user_id
)
yield "success", success
except Exception as e:
yield "error", handle_tweepy_exception(e)

View File

@ -0,0 +1,493 @@
from typing import cast
from backend.blocks.twitter._serializer import IncludesSerializer, ResponseDataSerializer
import tweepy
from tweepy.client import Response
from backend.data.block import Block, BlockCategory, BlockOutput, BlockSchema
from backend.data.model import SchemaField
from backend.blocks.twitter._builders import UserExpansionsBuilder
from backend.blocks.twitter._types import TweetUserFields, TweetFields, UserExpansionInputs, UserExpansions
from backend.blocks.twitter.tweepy_exceptions import handle_tweepy_exception
from backend.blocks.twitter._auth import (
TEST_CREDENTIALS,
TEST_CREDENTIALS_INPUT,
TwitterCredentials,
TwitterCredentialsField,
TwitterCredentialsInput,
)
class TwitterUnfollowUserBlock(Block):
"""
Allows a user to unfollow another user specified by target user ID
"""
class Input(BlockSchema):
credentials: TwitterCredentialsInput = TwitterCredentialsField(
["users.read", "users.write","follows.write", "offline.access"]
)
target_user_id: str = SchemaField(
description="The user ID of the user that you would like to unfollow",
placeholder="Enter target user ID"
)
class Output(BlockSchema):
success: bool = SchemaField(description="Whether the unfollow action was successful")
error: str = SchemaField(description="Error message if the request failed")
def __init__(self):
super().__init__(
id="37e386a4-a631-11ef-b7bd-b78204b35fa4",
description="This block unfollows a specified Twitter user.",
categories={BlockCategory.SOCIAL},
input_schema=TwitterUnfollowUserBlock.Input,
output_schema=TwitterUnfollowUserBlock.Output,
test_input={
"target_user_id": "12345",
"credentials": TEST_CREDENTIALS_INPUT
},
test_credentials=TEST_CREDENTIALS,
test_output=[
("success", True),
],
)
@staticmethod
def unfollow_user(
credentials: TwitterCredentials,
target_user_id: str
):
try:
client = tweepy.Client(
bearer_token=credentials.access_token.get_secret_value()
)
client.unfollow_user(target_user_id=target_user_id,user_auth=False)
return True
except tweepy.TweepyException:
raise
def run(
self,
input_data: Input,
*,
credentials: TwitterCredentials,
**kwargs,
) -> BlockOutput:
try:
success = self.unfollow_user(
credentials,
input_data.target_user_id
)
yield "success", success
except Exception as e:
yield "error", handle_tweepy_exception(e)
class TwitterFollowUserBlock(Block):
"""
Allows a user to follow another user specified by target user ID
"""
class Input(BlockSchema):
credentials: TwitterCredentialsInput = TwitterCredentialsField(
["users.read", "users.write","follows.write", "offline.access"]
)
target_user_id: str = SchemaField(
description="The user ID of the user that you would like to follow",
placeholder="Enter target user ID"
)
class Output(BlockSchema):
success: bool = SchemaField(description="Whether the follow action was successful")
error: str = SchemaField(description="Error message if the request failed")
def __init__(self):
super().__init__(
id="1aae6a5e-a631-11ef-a090-435900c6d429",
description="This block follows a specified Twitter user.",
categories={BlockCategory.SOCIAL},
input_schema=TwitterFollowUserBlock.Input,
output_schema=TwitterFollowUserBlock.Output,
test_input={
"target_user_id": "12345",
"credentials": TEST_CREDENTIALS_INPUT
},
test_credentials=TEST_CREDENTIALS,
test_output=[
("success", True),
("error", "")
],
)
@staticmethod
def follow_user(
credentials: TwitterCredentials,
target_user_id: str
):
try:
client = tweepy.Client(
bearer_token=credentials.access_token.get_secret_value()
)
client.follow_user(target_user_id=target_user_id,user_auth=False)
return True
except tweepy.TweepyException:
raise
def run(
self,
input_data: Input,
*,
credentials: TwitterCredentials,
**kwargs,
) -> BlockOutput:
try:
success = self.follow_user(
credentials,
input_data.target_user_id
)
yield "success", success
except Exception as e:
yield "error", handle_tweepy_exception(e)
class TwitterGetFollowersBlock(Block):
"""
Retrieves a list of followers for a specified Twitter user ID
"""
class Input(UserExpansionInputs):
credentials: TwitterCredentialsInput = TwitterCredentialsField(
["users.read", "offline.access","follows.read"]
)
target_user_id: str = SchemaField(
description="The user ID whose followers you would like to retrieve",
placeholder="Enter target user ID"
)
max_results: int = SchemaField(
description="Maximum number of results to return (1-1000, default 100)",
placeholder="Enter max results",
default=10,
advanced=True
)
pagination_token: str = SchemaField(
description="Token for retrieving next/previous page of results",
placeholder="Enter pagination token",
default="",
advanced=True
)
class Output(BlockSchema):
ids: list[str] = SchemaField(description="List of follower user IDs")
usernames: list[str] = SchemaField(description="List of follower usernames")
next_token: str = SchemaField(description="Next token for pagination")
data: list[dict] = SchemaField(description="Complete user data for followers")
includes: dict = SchemaField(description="Additional data requested via expansions")
meta: dict = SchemaField(description="Metadata including pagination info")
error: str = SchemaField(description="Error message if the request failed")
def __init__(self):
super().__init__(
id="30f66410-a631-11ef-8fe7-d7f888b4f43c",
description="This block retrieves followers of a specified Twitter user.",
categories={BlockCategory.SOCIAL},
input_schema=TwitterGetFollowersBlock.Input,
output_schema=TwitterGetFollowersBlock.Output,
test_input={
"target_user_id": "12345",
"max_results": 10,
"pagination_token": "",
"expansions": [],
"tweet_fields": [],
"user_fields": [],
"credentials": TEST_CREDENTIALS_INPUT
},
test_credentials=TEST_CREDENTIALS,
test_output=[
("ids", ["1234567890"]),
("usernames", ["testuser"]),
("data", [{"id": "1234567890", "username": "testuser"}]),
("includes", {}),
("meta", {"result_count": 1}),
("next_token", "next_token_value")
],
test_mock={
"get_followers": lambda *args, **kwargs: (
["1234567890"],
["testuser"],
[{"id": "1234567890", "username": "testuser"}],
{},
{"result_count": 1},
"next_token_value"
)
}
)
@staticmethod
def get_followers(
credentials: TwitterCredentials,
target_user_id: str,
max_results: int,
pagination_token: str,
expansions: list[UserExpansions],
tweet_fields: list[TweetFields],
user_fields: list[TweetUserFields],
):
try:
client = tweepy.Client(
bearer_token=credentials.access_token.get_secret_value()
)
params = {
"id": target_user_id,
"max_results": max_results,
"pagination_token": None if pagination_token == "" else pagination_token,
"user_auth": False
}
params = (UserExpansionsBuilder(params)
.add_expansions(expansions)
.add_tweet_fields(tweet_fields)
.add_user_fields(user_fields)
.build())
response = cast(
Response,
client.get_users_followers(
**params
)
)
meta = {}
follower_ids = []
follower_usernames = []
next_token = None
if response.meta:
meta = response.meta
next_token = meta.get("next_token")
included = IncludesSerializer.serialize(response.includes)
data = ResponseDataSerializer.serialize_list(response.data)
if response.data:
follower_ids = [str(user.id) for user in response.data]
follower_usernames = [user.username for user in response.data]
return follower_ids, follower_usernames, data, included, meta, next_token
raise Exception("Followers not found")
except tweepy.TweepyException:
raise
def run(
self,
input_data: Input,
*,
credentials: TwitterCredentials,
**kwargs,
) -> BlockOutput:
try:
ids, usernames, data, includes, meta, next_token = self.get_followers(
credentials,
input_data.target_user_id,
input_data.max_results,
input_data.pagination_token,
input_data.expansions,
input_data.tweet_fields,
input_data.user_fields
)
if ids:
yield "ids", ids
if usernames:
yield "usernames", usernames
if next_token:
yield "next_token", next_token
if data:
yield "data", data
if includes:
yield "includes", includes
if meta:
yield "meta", meta
except Exception as e:
yield "error", handle_tweepy_exception(e)
class TwitterGetFollowingBlock(Block):
"""
Retrieves a list of users that a specified Twitter user ID is following
"""
class Input(UserExpansionInputs):
credentials: TwitterCredentialsInput = TwitterCredentialsField(
["users.read", "offline.access","follows.read"]
)
target_user_id: str = SchemaField(
description="The user ID whose following you would like to retrieve",
placeholder="Enter target user ID"
)
max_results: int = SchemaField(
description="Maximum number of results to return (1-1000, default 100)",
placeholder="Enter max results",
default=10,
advanced=True
)
pagination_token: str = SchemaField(
description="Token for retrieving next/previous page of results",
placeholder="Enter pagination token",
default="",
advanced=True
)
class Output(BlockSchema):
ids: list[str] = SchemaField(description="List of following user IDs")
usernames: list[str] = SchemaField(description="List of following usernames")
next_token: str = SchemaField(description="Next token for pagination")
data: list[dict] = SchemaField(description="Complete user data for following")
includes: dict = SchemaField(description="Additional data requested via expansions")
meta: dict = SchemaField(description="Metadata including pagination info")
error: str = SchemaField(description="Error message if the request failed")
def __init__(self):
super().__init__(
id="264a399c-a631-11ef-a97d-bfde4ca91173",
description="This block retrieves the users that a specified Twitter user is following.",
categories={BlockCategory.SOCIAL},
input_schema=TwitterGetFollowingBlock.Input,
output_schema=TwitterGetFollowingBlock.Output,
test_input={
"target_user_id": "12345",
"max_results": 10,
"pagination_token": "",
"expansions": [],
"tweet_fields": [],
"user_fields": [],
"credentials": TEST_CREDENTIALS_INPUT
},
test_credentials=TEST_CREDENTIALS,
test_output=[
("ids", ["1234567890"]),
("usernames", ["testuser"]),
("data", [{"id": "1234567890", "username": "testuser"}]),
("includes", {}),
("meta", {"result_count": 1}),
("next_token", "next_token_value")
],
test_mock={
"get_following": lambda *args, **kwargs: (
["1234567890"],
["testuser"],
[{"id": "1234567890", "username": "testuser"}],
{},
{"result_count": 1},
"next_token_value"
)
}
)
@staticmethod
def get_following(
credentials: TwitterCredentials,
target_user_id: str,
max_results: int,
pagination_token: str,
expansions: list[UserExpansions],
tweet_fields: list[TweetFields],
user_fields: list[TweetUserFields],
):
try:
client = tweepy.Client(
bearer_token=credentials.access_token.get_secret_value()
)
params = {
"id": target_user_id,
"max_results": max_results,
"pagination_token": None if pagination_token == "" else pagination_token,
"user_auth": False
}
params = (UserExpansionsBuilder(params)
.add_expansions(expansions)
.add_tweet_fields(tweet_fields)
.add_user_fields(user_fields)
.build())
response = cast(
Response,
client.get_users_following(
**params
)
)
meta = {}
following_ids = []
following_usernames = []
next_token = None
if response.meta:
meta = response.meta
next_token = meta.get("next_token")
included = IncludesSerializer.serialize(response.includes)
data = ResponseDataSerializer.serialize_list(response.data)
if response.data:
following_ids = [str(user.id) for user in response.data]
following_usernames = [user.username for user in response.data]
return following_ids, following_usernames, data, included, meta, next_token
raise Exception("Following not found")
except tweepy.TweepyException:
raise
def run(
self,
input_data: Input,
*,
credentials: TwitterCredentials,
**kwargs,
) -> BlockOutput:
try:
ids, usernames, data, includes, meta, next_token = self.get_following(
credentials,
input_data.target_user_id,
input_data.max_results,
input_data.pagination_token,
input_data.expansions,
input_data.tweet_fields,
input_data.user_fields
)
if ids:
yield "ids", ids
if usernames:
yield "usernames", usernames
if next_token:
yield "next_token", next_token
if data:
yield "data", data
if includes:
yield "includes", includes
if meta:
yield "meta", meta
except Exception as e:
yield "error", handle_tweepy_exception(e)

View File

@ -0,0 +1,316 @@
from typing import cast
from backend.blocks.twitter._serializer import IncludesSerializer, ResponseDataSerializer
import tweepy
from tweepy.client import Response
from backend.data.block import Block, BlockCategory, BlockOutput, BlockSchema
from backend.data.model import SchemaField
from backend.blocks.twitter._builders import UserExpansionsBuilder
from backend.blocks.twitter._types import TweetFields, TweetUserFields, UserExpansionInputs, UserExpansions
from backend.blocks.twitter.tweepy_exceptions import handle_tweepy_exception
from backend.blocks.twitter._auth import (
TEST_CREDENTIALS,
TEST_CREDENTIALS_INPUT,
TwitterCredentials,
TwitterCredentialsField,
TwitterCredentialsInput,
)
class TwitterUnmuteUserBlock(Block):
"""
Allows a user to unmute another user specified by target user ID
"""
class Input(BlockSchema):
credentials: TwitterCredentialsInput = TwitterCredentialsField(
["users.read", "users.write", "offline.access"]
)
target_user_id: str = SchemaField(
description="The user ID of the user that you would like to unmute",
placeholder="Enter target user ID"
)
class Output(BlockSchema):
success: bool = SchemaField(description="Whether the unmute action was successful")
error: str = SchemaField(description="Error message if the request failed")
def __init__(self):
super().__init__(
id="40458504-a631-11ef-940b-eff92be55422",
description="This block unmutes a specified Twitter user.",
categories={BlockCategory.SOCIAL},
input_schema=TwitterUnmuteUserBlock.Input,
output_schema=TwitterUnmuteUserBlock.Output,
test_input={
"target_user_id": "12345",
"credentials": TEST_CREDENTIALS_INPUT
},
test_credentials=TEST_CREDENTIALS,
test_output=[
("success", True),
],
)
@staticmethod
def unmute_user(
credentials: TwitterCredentials,
target_user_id: str
):
try:
client = tweepy.Client(
bearer_token=credentials.access_token.get_secret_value()
)
client.unmute(target_user_id=target_user_id,user_auth=False)
return True
except tweepy.TweepyException:
raise
def run(
self,
input_data: Input,
*,
credentials: TwitterCredentials,
**kwargs,
) -> BlockOutput:
try:
success = self.unmute_user(
credentials,
input_data.target_user_id
)
yield "success", success
except Exception as e:
yield "error", handle_tweepy_exception(e)
class TwitterGetMutedUsersBlock(Block):
"""
Returns a list of users who are muted by the authenticating user
"""
class Input(UserExpansionInputs):
credentials: TwitterCredentialsInput = TwitterCredentialsField(
["users.read", "offline.access"]
)
max_results: int = SchemaField(
description="The maximum number of results to be returned per page (1-1000). Default is 100.",
placeholder="Enter max results",
default=10,
advanced=True
)
pagination_token: str = SchemaField(
description="Token to request next/previous page of results",
placeholder="Enter pagination token",
default="",
advanced=True
)
class Output(BlockSchema):
ids: list[str] = SchemaField(description="List of muted user IDs")
usernames: list[str] = SchemaField(description="List of muted usernames")
next_token: str = SchemaField(description="Next token for pagination")
data: list[dict] = SchemaField(description="Complete user data for muted users")
includes: dict = SchemaField(description="Additional data requested via expansions")
meta: dict = SchemaField(description="Metadata including pagination info")
error: str = SchemaField(description="Error message if the request failed")
def __init__(self):
super().__init__(
id="475024da-a631-11ef-9ccd-f724b8b03cda",
description="This block gets a list of users muted by the authenticating user.",
categories={BlockCategory.SOCIAL},
input_schema=TwitterGetMutedUsersBlock.Input,
output_schema=TwitterGetMutedUsersBlock.Output,
test_input={
"max_results": 10,
"pagination_token": "",
"credentials": TEST_CREDENTIALS_INPUT,
"expansions": [],
"tweet_fields": [],
"user_fields": []
},
test_credentials=TEST_CREDENTIALS,
test_output=[
("ids", ["12345", "67890"]),
("usernames", ["testuser1", "testuser2"]),
("data", [
{"id": "12345", "username": "testuser1"},
{"id": "67890", "username": "testuser2"}
]),
("includes", {}),
("meta", {"next_token": "next_token_value"}),
("next_token", "next_token_value")
],
test_mock={
"get_muted_users": lambda *args, **kwargs: (
["12345", "67890"],
["testuser1", "testuser2"],
[{"id": "12345", "username": "testuser1"}, {"id": "67890", "username": "testuser2"}],
{},
{"next_token": "next_token_value"},
"next_token_value"
)
}
)
@staticmethod
def get_muted_users(
credentials: TwitterCredentials,
max_results: int,
pagination_token: str,
expansions: list[UserExpansions],
tweet_fields: list[TweetFields],
user_fields: list[TweetUserFields],
):
try:
client = tweepy.Client(
bearer_token=credentials.access_token.get_secret_value()
)
params = {
"max_results": max_results,
"pagination_token": None if pagination_token == "" else pagination_token,
"user_auth": False
}
params = (UserExpansionsBuilder(params)
.add_expansions(expansions)
.add_tweet_fields(tweet_fields)
.add_user_fields(user_fields)
.build())
response = cast(
Response,
client.get_muted(**params)
)
meta = {}
user_ids = []
usernames = []
next_token = None
if response.meta:
meta = response.meta
next_token = meta.get("next_token")
included = IncludesSerializer.serialize(response.includes)
data = ResponseDataSerializer.serialize_list(response.data)
if response.data:
user_ids = [str(item.id) for item in response.data]
usernames = [item.username for item in response.data]
return user_ids, usernames, data, included, meta, next_token
raise Exception("Muted users not found")
except tweepy.TweepyException:
raise
def run(
self,
input_data: Input,
*,
credentials: TwitterCredentials,
**kwargs,
) -> BlockOutput:
try:
ids, usernames, data, includes, meta, next_token = self.get_muted_users(
credentials,
input_data.max_results,
input_data.pagination_token,
input_data.expansions,
input_data.tweet_fields,
input_data.user_fields
)
if ids:
yield "ids", ids
if usernames:
yield "usernames", usernames
if next_token:
yield "next_token", next_token
if data:
yield "data", data
if includes:
yield "includes", includes
if meta:
yield "meta", meta
except Exception as e:
yield "error", handle_tweepy_exception(e)
class TwitterMuteUserBlock(Block):
"""
Allows a user to mute another user specified by target user ID
"""
class Input(BlockSchema):
credentials: TwitterCredentialsInput = TwitterCredentialsField(
["users.read", "users.write", "offline.access"]
)
target_user_id: str = SchemaField(
description="The user ID of the user that you would like to mute",
placeholder="Enter target user ID"
)
class Output(BlockSchema):
success: bool = SchemaField(description="Whether the mute action was successful")
error: str = SchemaField(description="Error message if the request failed")
def __init__(self):
super().__init__(
id="4d1919d0-a631-11ef-90ab-3b73af9ce8f1",
description="This block mutes a specified Twitter user.",
categories={BlockCategory.SOCIAL},
input_schema=TwitterMuteUserBlock.Input,
output_schema=TwitterMuteUserBlock.Output,
test_input={
"target_user_id": "12345",
"credentials": TEST_CREDENTIALS_INPUT
},
test_credentials=TEST_CREDENTIALS,
test_output=[
("success", True),
],
)
@staticmethod
def mute_user(
credentials: TwitterCredentials,
target_user_id: str
):
try:
client = tweepy.Client(
bearer_token=credentials.access_token.get_secret_value()
)
client.mute(target_user_id=target_user_id,user_auth=False)
return True
except tweepy.TweepyException:
raise
def run(
self,
input_data: Input,
*,
credentials: TwitterCredentials,
**kwargs,
) -> BlockOutput:
try:
success = self.mute_user(
credentials,
input_data.target_user_id
)
yield "success", success
except Exception as e:
yield "error", handle_tweepy_exception(e)

View File

@ -0,0 +1,306 @@
from typing import cast
from backend.blocks.twitter._serializer import IncludesSerializer, ResponseDataSerializer
import tweepy
from tweepy.client import Response
from backend.data.block import Block, BlockCategory, BlockOutput, BlockSchema
from backend.data.model import SchemaField
from backend.blocks.twitter._builders import UserExpansionsBuilder
from backend.blocks.twitter._types import TweetFields, TweetUserFields, UserExpansionInputs, UserExpansions
from backend.blocks.twitter.tweepy_exceptions import handle_tweepy_exception
from backend.blocks.twitter._auth import (
TEST_CREDENTIALS,
TEST_CREDENTIALS_INPUT,
TwitterCredentials,
TwitterCredentialsField,
TwitterCredentialsInput,
)
class TwitterGetUserBlock(Block):
"""
Gets information about a single Twitter user specified by ID or username
"""
class Input(UserExpansionInputs):
credentials: TwitterCredentialsInput = TwitterCredentialsField(
["users.read", "offline.access"]
)
user_id: str = SchemaField(
description="The ID of the user to lookup",
placeholder="Enter user ID",
default="",
advanced=False
)
username: str = SchemaField(
description="The Twitter username (handle) of the user",
placeholder="Enter username",
default="",
advanced=False
)
class Output(BlockSchema):
# Common outputs
id: str = SchemaField(description="User ID")
username_: str = SchemaField(description="User username")
name_: str = SchemaField(description="User name")
# Complete outputs
data: dict = SchemaField(description="Complete user data")
included: dict = SchemaField(description="Additional data requested via expansions")
error: str = SchemaField(description="Error message if the request failed")
def __init__(self):
super().__init__(
id="5446db8e-a631-11ef-812a-cf315d373ee9",
description="This block retrieves information about a specified Twitter user.",
categories={BlockCategory.SOCIAL},
input_schema=TwitterGetUserBlock.Input,
output_schema=TwitterGetUserBlock.Output,
test_input={
"user_id": None,
"username": "twitter",
"credentials": TEST_CREDENTIALS_INPUT,
"expansions": [],
"tweet_fields": [],
"user_fields": []
},
test_credentials=TEST_CREDENTIALS,
test_output=[
("id", "783214"),
("username_", "twitter"),
("name_", "Twitter"),
("data", {"user": {"id": "783214", "username": "twitter", "name": "Twitter"}}),
("included", {}),
("error", None)
],
)
@staticmethod
def get_user(
credentials: TwitterCredentials,
user_id: str,
username: str,
expansions: list[UserExpansions],
tweet_fields: list[TweetFields],
user_fields: list[TweetUserFields]
):
try:
client = tweepy.Client(
bearer_token=credentials.access_token.get_secret_value()
)
params = {
"id": None if not user_id else user_id,
"username": None if not username else username,
"user_auth": False,
}
params = (UserExpansionsBuilder(params)
.add_expansions(expansions)
.add_tweet_fields(tweet_fields)
.add_user_fields(user_fields)
.build())
print("params : ", params)
response = cast(
Response,
client.get_user(**params)
)
username = ""
id = ""
name = ""
included = IncludesSerializer.serialize(response.includes)
data = ResponseDataSerializer.serialize_dict(response.data)
if response.data:
username = response.data.username
id = str(response.data.id)
name = response.data.name
if username and id:
return data, included, username, id, name
else:
raise tweepy.TweepyException("User not found")
except tweepy.TweepyException:
raise
def run(
self,
input_data: Input,
*,
credentials: TwitterCredentials,
**kwargs,
) -> BlockOutput:
try:
data, included, username, id, name = self.get_user(
credentials,
input_data.user_id,
input_data.username,
input_data.expansions,
input_data.tweet_fields,
input_data.user_fields
)
if id:
yield "id", id
if username:
yield "username_", username
if name:
yield "name_", name
if data:
yield "data", data
if included:
yield "included", included
except Exception as e:
yield "error", handle_tweepy_exception(e)
class TwitterGetUsersBlock(Block):
"""
Gets information about multiple Twitter users specified by IDs or usernames
"""
class Input(UserExpansionInputs):
credentials: TwitterCredentialsInput = TwitterCredentialsField(
["users.read", "offline.access"]
)
user_ids: list[str] = SchemaField(
description="List of user IDs to lookup (max 100)",
placeholder="Enter user IDs",
default=[],
advanced=False
)
usernames: list[str] = SchemaField(
description="List of Twitter usernames/handles to lookup (max 100)",
placeholder="Enter usernames",
default=[],
advanced=False
)
class Output(BlockSchema):
# Common outputs
ids: list[str] = SchemaField(description="User IDs")
usernames_: list[str] = SchemaField(description="User usernames")
names_: list[str] = SchemaField(description="User names")
# Complete outputs
data: list[dict] = SchemaField(description="Complete users data")
included: dict = SchemaField(description="Additional data requested via expansions")
error: str = SchemaField(description="Error message if the request failed")
def __init__(self):
super().__init__(
id="5abc857c-a631-11ef-8cfc-f7b79354f7a1",
description="This block retrieves information about multiple Twitter users.",
categories={BlockCategory.SOCIAL},
input_schema=TwitterGetUsersBlock.Input,
output_schema=TwitterGetUsersBlock.Output,
test_input={
"user_ids": [],
"usernames": ["twitter", "twitterdev"],
"credentials": TEST_CREDENTIALS_INPUT,
"expansions": [],
"tweet_fields": [],
"user_fields": []
},
test_credentials=TEST_CREDENTIALS,
test_output=[
("ids", ["783214", "2244994945"]),
("usernames_", ["twitter", "twitterdev"]),
("names_", ["Twitter", "Twitter Dev"]),
("data", {"users": [
{"id": "783214", "username": "twitter", "name": "Twitter"},
{"id": "2244994945", "username": "twitterdev", "name": "Twitter Dev"}
]}),
("included", {}),
("error", None)
],
)
@staticmethod
def get_users(
credentials: TwitterCredentials,
user_ids: list[str],
usernames: list[str],
expansions: list[UserExpansions],
tweet_fields: list[TweetFields],
user_fields: list[TweetUserFields]
):
try:
client = tweepy.Client(
bearer_token=credentials.access_token.get_secret_value()
)
params = {
"ids": None if not user_ids else user_ids,
"usernames": None if not usernames else usernames,
"user_auth": False
}
params = (UserExpansionsBuilder(params)
.add_expansions(expansions)
.add_tweet_fields(tweet_fields)
.add_user_fields(user_fields)
.build())
response = cast(
Response,
client.get_users(**params)
)
usernames = []
ids = []
names = []
included = IncludesSerializer.serialize(response.includes)
data = ResponseDataSerializer.serialize_list(response.data)
if response.data:
for user in response.data:
usernames.append(user.username)
ids.append(str(user.id))
names.append(user.name)
if usernames and ids:
return data, included, usernames, ids, names
else:
raise tweepy.TweepyException("Users not found")
except tweepy.TweepyException:
raise
def run(
self,
input_data: Input,
*,
credentials: TwitterCredentials,
**kwargs,
) -> BlockOutput:
try:
data, included, usernames, ids, names = self.get_users(
credentials,
input_data.user_ids,
input_data.usernames,
input_data.expansions,
input_data.tweet_fields,
input_data.user_fields
)
if ids:
yield "ids", ids
if usernames:
yield "usernames_", usernames
if names:
yield "names_", names
if data:
yield "data", data
if included:
yield "included", included
except Exception as e:
yield "error", handle_tweepy_exception(e)

View File

@ -136,6 +136,7 @@ def SchemaField(
placeholder: Optional[str] = None,
advanced: Optional[bool] = False,
secret: bool = False,
is_multi_select: bool = False,
exclude: bool = False,
hidden: Optional[bool] = None,
depends_on: list[str] | None = None,

View File

@ -1,6 +1,8 @@
import secrets
import hashlib
import base64
from datetime import datetime, timedelta, timezone
from typing import TYPE_CHECKING
from typing import TYPE_CHECKING, Optional
from pydantic import SecretStr
@ -171,6 +173,8 @@ class IntegrationCredentialsStore:
def update_creds(self, user_id: str, updated: Credentials) -> None:
with self.locked_user_integrations(user_id):
print("user_id : ", user_id)
print("updated : ", updated)
current = self.get_creds_by_id(user_id, updated.id)
if not current:
raise ValueError(
@ -210,18 +214,24 @@ class IntegrationCredentialsStore:
]
self._set_user_integration_creds(user_id, filtered_credentials)
def store_state_token(self, user_id: str, provider: str, scopes: list[str]) -> str:
def store_state_token(self, user_id: str, provider: str, scopes: list[str]) -> tuple[str, str]:
token = secrets.token_urlsafe(32)
expires_at = datetime.now(timezone.utc) + timedelta(minutes=10)
code_verifier = self._generate_code_verifier(128)
code_challenge = self._generate_code_challenge(code_verifier)
print("login code_verifier: ", code_verifier)
state = OAuthState(
token=token,
provider=provider,
code_verifier= code_verifier,
expires_at=int(expires_at.timestamp()),
scopes=scopes,
)
with self.locked_user_integrations(user_id):
user_integrations = self._get_user_integrations(user_id)
oauth_states = user_integrations.oauth_states
oauth_states.append(state)
@ -231,7 +241,24 @@ class IntegrationCredentialsStore:
user_id=user_id, data=user_integrations
)
return token
return token, code_challenge
def _generate_code_verifier(self, length: int) -> str:
"""
Generate a secure random code verifier of specified length.
"""
return secrets.token_urlsafe(length)
def _generate_code_challenge(self, code_verifier: str, method: str = "S256") -> str:
"""
Generate code challenge using SHA256 from the code verifier.
"""
if method != "S256":
raise ValueError(f"Unsupported code challenge method: {method}")
sha256_hash = hashlib.sha256(code_verifier.encode('utf-8')).digest()
code_challenge = base64.urlsafe_b64encode(sha256_hash).decode('utf-8')
return code_challenge.replace('=', '')
def get_any_valid_scopes_from_state_token(
self, user_id: str, token: str, provider: str
@ -263,6 +290,26 @@ class IntegrationCredentialsStore:
return []
def _get_code_verifier(self, user_id: str, provider: str, token : str) -> Optional[str]:
user_integrations = self._get_user_integrations(user_id)
oauth_states = user_integrations.oauth_states
now = datetime.now(timezone.utc)
valid_state = next(
(
state
for state in oauth_states
if state.token == token
and state.provider == provider
and state.expires_at > now.timestamp()
),
None,
)
if valid_state:
return valid_state.code_verifier
def verify_state_token(self, user_id: str, token: str, provider: str) -> bool:
with self.locked_user_integrations(user_id):
user_integrations = self._get_user_integrations(user_id)

View File

@ -3,6 +3,7 @@ from typing import TYPE_CHECKING
from .github import GitHubOAuthHandler
from .google import GoogleOAuthHandler
from .notion import NotionOAuthHandler
from .twitter import TwitterOAuthHandler
if TYPE_CHECKING:
from ..providers import ProviderName
@ -15,6 +16,7 @@ HANDLERS_BY_NAME: dict["ProviderName", type["BaseOAuthHandler"]] = {
GitHubOAuthHandler,
GoogleOAuthHandler,
NotionOAuthHandler,
TwitterOAuthHandler,
]
}
# --8<-- [end:HANDLERS_BY_NAMEExample]

View File

@ -1,7 +1,7 @@
import logging
import time
from abc import ABC, abstractmethod
from typing import ClassVar
from typing import ClassVar, Optional
from backend.data.model import OAuth2Credentials
from backend.integrations.providers import ProviderName
@ -23,7 +23,9 @@ class BaseOAuthHandler(ABC):
@abstractmethod
# --8<-- [start:BaseOAuthHandler3]
def get_login_url(self, scopes: list[str], state: str) -> str:
def get_login_url(
self, scopes: list[str], state: str, code_challenge: Optional[str]
) -> str:
# --8<-- [end:BaseOAuthHandler3]
"""Constructs a login URL that the user can be redirected to"""
...
@ -31,7 +33,7 @@ class BaseOAuthHandler(ABC):
@abstractmethod
# --8<-- [start:BaseOAuthHandler4]
def exchange_code_for_tokens(
self, code: str, scopes: list[str]
self, code: str, scopes: list[str], code_verifier: Optional[str]
) -> OAuth2Credentials:
# --8<-- [end:BaseOAuthHandler4]
"""Exchanges the acquired authorization code from login for a set of tokens"""

View File

@ -34,7 +34,9 @@ class GitHubOAuthHandler(BaseOAuthHandler):
self.token_url = "https://github.com/login/oauth/access_token"
self.revoke_url = "https://api.github.com/applications/{client_id}/token"
def get_login_url(self, scopes: list[str], state: str) -> str:
def get_login_url(
self, scopes: list[str], state: str, code_challenge: Optional[str]
) -> str:
params = {
"client_id": self.client_id,
"redirect_uri": self.redirect_uri,
@ -44,7 +46,7 @@ class GitHubOAuthHandler(BaseOAuthHandler):
return f"{self.auth_base_url}?{urlencode(params)}"
def exchange_code_for_tokens(
self, code: str, scopes: list[str]
self, code: str, scopes: list[str], code_verifier: Optional[str]
) -> OAuth2Credentials:
return self._request_tokens({"code": code, "redirect_uri": self.redirect_uri})

View File

@ -1,4 +1,5 @@
import logging
from typing import Optional
from google.auth.external_account_authorized_user import (
Credentials as ExternalAccountCredentials,
@ -38,7 +39,9 @@ class GoogleOAuthHandler(BaseOAuthHandler):
self.token_uri = "https://oauth2.googleapis.com/token"
self.revoke_uri = "https://oauth2.googleapis.com/revoke"
def get_login_url(self, scopes: list[str], state: str) -> str:
def get_login_url(
self, scopes: list[str], state: str, code_challenge: Optional[str]
) -> str:
all_scopes = list(set(scopes + self.DEFAULT_SCOPES))
logger.debug(f"Setting up OAuth flow with scopes: {all_scopes}")
flow = self._setup_oauth_flow(all_scopes)
@ -52,7 +55,7 @@ class GoogleOAuthHandler(BaseOAuthHandler):
return authorization_url
def exchange_code_for_tokens(
self, code: str, scopes: list[str]
self, code: str, scopes: list[str], code_verifier: Optional[str]
) -> OAuth2Credentials:
logger.debug(f"Exchanging code for tokens with scopes: {scopes}")

View File

@ -1,4 +1,5 @@
from base64 import b64encode
from typing import Optional
from urllib.parse import urlencode
from backend.data.model import OAuth2Credentials
@ -26,7 +27,9 @@ class NotionOAuthHandler(BaseOAuthHandler):
self.auth_base_url = "https://api.notion.com/v1/oauth/authorize"
self.token_url = "https://api.notion.com/v1/oauth/token"
def get_login_url(self, scopes: list[str], state: str) -> str:
def get_login_url(
self, scopes: list[str], state: str, code_challenge: Optional[str]
) -> str:
params = {
"client_id": self.client_id,
"redirect_uri": self.redirect_uri,
@ -37,7 +40,7 @@ class NotionOAuthHandler(BaseOAuthHandler):
return f"{self.auth_base_url}?{urlencode(params)}"
def exchange_code_for_tokens(
self, code: str, scopes: list[str]
self, code: str, scopes: list[str], code_verifier: Optional[str]
) -> OAuth2Credentials:
request_body = {
"grant_type": "authorization_code",

View File

@ -0,0 +1,171 @@
import time
import urllib.parse
from typing import ClassVar, Optional
import requests
from autogpt_libs.supabase_integration_credentials_store import OAuth2Credentials
from backend.integrations.oauth.base import BaseOAuthHandler
class TwitterOAuthHandler(BaseOAuthHandler):
PROVIDER_NAME: ClassVar[str] = "twitter"
DEFAULT_SCOPES: ClassVar[list[str]] = [
"tweet.read",
"tweet.write",
"tweet.moderate.write",
"users.read",
"follows.read",
"follows.write",
"offline.access",
"space.read",
"mute.read",
"mute.write",
"like.read",
"like.write",
"list.read",
"list.write",
"block.read",
"block.write",
"bookmark.read",
"bookmark.write"
]
AUTHORIZE_URL = "https://twitter.com/i/oauth2/authorize"
TOKEN_URL = "https://api.x.com/2/oauth2/token"
USERNAME_URL = "https://api.x.com/2/users/me"
REVOKE_URL = "https://api.x.com/2/oauth2/revoke"
def __init__(self, client_id: str, client_secret: str, redirect_uri: str):
self.client_id = client_id
self.client_secret = client_secret
self.redirect_uri = redirect_uri
def get_login_url(
self, scopes: list[str], state: str, code_challenge: Optional[str]
) -> str:
"""Generate Twitter OAuth 2.0 authorization URL"""
# scopes = self.handle_default_scopes(scopes)
if code_challenge is None:
raise ValueError("code_verifier is required for Twitter OAuth")
params = {
"response_type": "code",
"client_id": self.client_id,
"redirect_uri": self.redirect_uri,
"scope": " ".join(self.DEFAULT_SCOPES),
"state": state,
"code_challenge": code_challenge,
"code_challenge_method": "S256",
}
return f"{self.AUTHORIZE_URL}?{urllib.parse.urlencode(params)}"
def exchange_code_for_tokens(
self, code: str, scopes: list[str], code_verifier: Optional[str]
) -> OAuth2Credentials:
"""Exchange authorization code for access tokens"""
headers = {"Content-Type": "application/x-www-form-urlencoded"}
data = {
"code": code,
"grant_type": "authorization_code",
"redirect_uri": self.redirect_uri,
"code_verifier": code_verifier,
}
auth = (self.client_id, self.client_secret)
response = requests.post(self.TOKEN_URL, headers=headers, data=data, auth=auth)
response.raise_for_status()
tokens = response.json()
username = self._get_username(tokens["access_token"])
return OAuth2Credentials(
provider=self.PROVIDER_NAME,
title=None,
username=username,
access_token=tokens["access_token"],
refresh_token=tokens.get("refresh_token"),
access_token_expires_at=int(time.time()) + tokens["expires_in"],
refresh_token_expires_at=None,
scopes=scopes,
)
def _get_username(self, access_token: str) -> str:
"""Get the username from the access token"""
headers = {"Authorization": f"Bearer {access_token}"}
params = {"user.fields": "username"}
response = requests.get(
f"{self.USERNAME_URL}?{urllib.parse.urlencode(params)}", headers=headers
)
response.raise_for_status()
return response.json()["data"]["username"]
def _refresh_tokens(self, credentials: OAuth2Credentials) -> OAuth2Credentials:
"""Refresh access tokens using refresh token"""
if not credentials.refresh_token:
raise ValueError("No refresh token available")
header = {"Content-Type": "application/x-www-form-urlencoded"}
data = {
"grant_type": "refresh_token",
"refresh_token": credentials.refresh_token.get_secret_value(),
}
auth = (self.client_id, self.client_secret)
response = requests.post(self.TOKEN_URL, headers=header, data=data, auth = auth)
try:
response.raise_for_status()
except requests.exceptions.HTTPError as e:
print("HTTP Error:", e)
print("Response Content:", response.text)
raise
tokens = response.json()
username = self._get_username(tokens["access_token"])
return OAuth2Credentials(
id=credentials.id,
provider=self.PROVIDER_NAME,
title=None,
username=username,
access_token=tokens["access_token"],
refresh_token=tokens["refresh_token"],
access_token_expires_at=int(time.time()) + tokens["expires_in"],
scopes=credentials.scopes,
refresh_token_expires_at=None,
)
def revoke_tokens(self, credentials: OAuth2Credentials) -> bool:
"""Revoke the access token"""
header = {"Content-Type": "application/x-www-form-urlencoded"}
data = {
"token": credentials.access_token.get_secret_value(),
"token_type_hint": "access_token"
}
auth = (self.client_id, self.client_secret)
response = requests.post(self.REVOKE_URL,headers=header, data=data, auth=auth)
try:
response.raise_for_status()
except requests.exceptions.HTTPError as e:
print("HTTP Error:", e)
print("Response Content:", response.text)
raise
return response.status_code == 200

View File

@ -60,11 +60,12 @@ def login(
requested_scopes = scopes.split(",") if scopes else []
# Generate and store a secure random state token along with the scopes
state_token = creds_manager.store.store_state_token(
state_token, code_challenge = creds_manager.store.store_state_token(
user_id, provider, requested_scopes
)
login_url = handler.get_login_url(requested_scopes, state_token)
login_url = handler.get_login_url(
requested_scopes, state_token, code_challenge=code_challenge
)
return LoginResponse(login_url=login_url, state_token=state_token)
@ -90,6 +91,8 @@ def callback(
) -> CredentialsMetaResponse:
logger.debug(f"Received OAuth callback for provider: {provider}")
handler = _get_provider_oauth_handler(request, provider)
code_verifier = creds_manager.store._get_code_verifier(user_id, provider,state_token)
# Verify the state token
if not creds_manager.store.verify_state_token(user_id, state_token, provider):
@ -104,7 +107,8 @@ def callback(
scopes = handler.handle_default_scopes(scopes)
credentials = handler.exchange_code_for_tokens(code, scopes)
credentials = handler.exchange_code_for_tokens(code, scopes, code_verifier)
logger.debug(f"Received credentials with final scopes: {credentials.scopes}")
# Check if the granted scopes are sufficient for the requested scopes

View File

@ -259,6 +259,10 @@ class Secrets(UpdateTrackingModel["Secrets"], BaseSettings):
notion_client_secret: str = Field(
default="", description="Notion OAuth client secret"
)
twitter_client_id: str = Field(default="", description="Twitter/X OAuth client ID")
twitter_client_secret: str = Field(
default="", description="Twitter/X OAuth client secret"
)
openai_api_key: str = Field(default="", description="OpenAI API key")
anthropic_api_key: str = Field(default="", description="Anthropic API key")

View File

@ -297,6 +297,9 @@ export function CustomNode({
const newValues = JSON.parse(JSON.stringify(data.hardcodedValues));
let current = newValues;
console.log("Keys: ", keys);
console.log("Value: ", newValues);
for (let i = 0; i < keys.length - 1; i++) {
const { key: currentKey, index } = keys[i];
if (index !== undefined) {

View File

@ -7,8 +7,15 @@ import SchemaTooltip from "@/components/SchemaTooltip";
import useCredentials from "@/hooks/useCredentials";
import { zodResolver } from "@hookform/resolvers/zod";
import { NotionLogoIcon } from "@radix-ui/react-icons";
import { FaDiscord, FaGithub, FaGoogle, FaMedium, FaKey } from "react-icons/fa";
import { FC, useState } from "react";
import {
FaDiscord,
FaGithub,
FaTwitter,
FaGoogle,
FaMedium,
FaKey,
} from "react-icons/fa";
import { FC, useMemo, useState } from "react";
import {
CredentialsMetaInput,
CredentialsProviderName,
@ -71,6 +78,7 @@ export const providerIcons: Record<
unreal_speech: fallbackIcon,
exa: fallbackIcon,
hubspot: fallbackIcon,
twitter: FaTwitter,
};
// --8<-- [end:ProviderIconsEmbed]

View File

@ -40,6 +40,7 @@ const providerDisplayNames: Record<CredentialsProviderName, string> = {
unreal_speech: "Unreal Speech",
exa: "Exa",
hubspot: "Hubspot",
twitter: "Twitter",
} as const;
// --8<-- [end:CredentialsProviderNames]

View File

@ -17,6 +17,7 @@ import {
BlockIOStringSubSchema,
BlockIONumberSubSchema,
BlockIOBooleanSubSchema,
BlockIOSimpleTypeSubSchema,
} from "@/lib/autogpt-server-api/types";
import React, { FC, useCallback, useEffect, useMemo, useState } from "react";
import { Button } from "./ui/button";
@ -40,6 +41,7 @@ import { LocalValuedInput } from "./ui/input";
import NodeHandle from "./NodeHandle";
import { ConnectionData } from "./CustomNode";
import { CredentialsInput } from "./integrations/credentials-input";
import { MultiSelect } from "./ui/multiselect-input";
type NodeObjectInputTreeProps = {
nodeId: string;
@ -826,6 +828,13 @@ const NodeKeyValueInput: FC<{
);
};
// Checking if schema is type of string
function isStringSubSchema(
schema: BlockIOSimpleTypeSubSchema,
): schema is BlockIOStringSubSchema {
return "type" in schema && schema.type === "string";
}
const NodeArrayInput: FC<{
nodeId: string;
selfKey: string;
@ -852,6 +861,26 @@ const NodeArrayInput: FC<{
entries ??= schema.default;
if (!entries || !Array.isArray(entries)) entries = [];
const isMultiSelectEnum =
schema.items &&
isStringSubSchema(schema.items) &&
schema.items.enum &&
schema.isMultiSelect;
if (isMultiSelectEnum) {
return (
<NodeMultiSelectInput
selfKey={selfKey}
schema={schema.items! as BlockIOStringSubSchema}
value={entries}
error={errors[selfKey]}
handleInputChange={handleInputChange}
className={className}
displayName={displayName || beautifyString(selfKey)}
/>
);
}
const prefix = `${selfKey}_$_`;
connections
.filter((c) => c.targetHandle.startsWith(prefix) && c.target === nodeId)

View File

@ -0,0 +1,307 @@
// This is a special version of multi select for build page only
import * as React from "react";
import { cva, type VariantProps } from "class-variance-authority";
import { CheckIcon, XCircle, ChevronDown, XIcon } from "lucide-react";
import { cn } from "@/lib/utils";
import { Separator } from "@/components/ui/separator";
import { Button } from "@/components/ui/button";
import { Badge } from "@/components/ui/badge";
import {
Popover,
PopoverContent,
PopoverTrigger,
} from "@/components/ui/popover";
import {
Command,
CommandEmpty,
CommandGroup,
CommandInput,
CommandItem,
CommandList,
CommandSeparator,
} from "@/components/ui/command";
const multiSelectVariants = cva(
"m-1 transition ease-in-out delay-150 hover:-translate-y-1 hover:scale-110 duration-300",
{
variants: {
variant: {
default:
"border-foreground/10 text-foreground bg-card hover:bg-card/80",
secondary:
"border-foreground/10 bg-secondary text-secondary-foreground hover:bg-secondary/80",
destructive:
"border-transparent bg-destructive text-destructive-foreground hover:bg-destructive/80",
inverted: "inverted",
},
},
defaultVariants: {
variant: "default",
},
},
);
/**
* Props for MultiSelect component
*/
interface MultiSelectProps
extends React.ButtonHTMLAttributes<HTMLButtonElement>,
VariantProps<typeof multiSelectVariants> {
options: string[];
onValueChange: (value: string[]) => void;
defaultValue?: string[];
placeholder?: string;
animation?: number;
maxCount?: number;
modalPopover?: boolean;
asChild?: boolean;
className?: string;
}
export const MultiSelect = React.forwardRef<
HTMLButtonElement,
MultiSelectProps
>(
(
{
options,
onValueChange,
variant,
defaultValue = [],
placeholder = "Select options",
animation = 2,
maxCount = 2,
modalPopover = false,
asChild = false,
className,
...props
},
ref,
) => {
const [selectedValues, setSelectedValues] =
React.useState<string[]>(defaultValue);
const [isPopoverOpen, setIsPopoverOpen] = React.useState(false);
const [isAnimating, setIsAnimating] = React.useState(false);
const handleInputKeyDown = (
event: React.KeyboardEvent<HTMLInputElement>,
) => {
if (event.key === "Enter") {
setIsPopoverOpen(true);
} else if (event.key === "Backspace" && !event.currentTarget.value) {
const newSelectedValues = [...selectedValues];
newSelectedValues.pop();
setSelectedValues(newSelectedValues);
onValueChange(newSelectedValues);
}
};
const toggleOption = (option: string) => {
const newSelectedValues = selectedValues.includes(option)
? selectedValues.filter((value) => value !== option)
: [...selectedValues, option];
setSelectedValues(newSelectedValues);
onValueChange(newSelectedValues);
};
const handleClear = () => {
setSelectedValues([]);
onValueChange([]);
};
const handleTogglePopover = () => {
setIsPopoverOpen((prev) => !prev);
};
const clearExtraOptions = () => {
const newSelectedValues = selectedValues.slice(0, maxCount);
setSelectedValues(newSelectedValues);
onValueChange(newSelectedValues);
};
const toggleAll = () => {
if (selectedValues.length === options.length) {
handleClear();
} else {
const allValues = options.map((option) => option);
setSelectedValues(allValues);
onValueChange(allValues);
}
};
return (
<Popover
open={isPopoverOpen}
onOpenChange={setIsPopoverOpen}
modal={modalPopover}
>
<PopoverTrigger asChild>
<Button
ref={ref}
{...props}
onClick={handleTogglePopover}
className={cn(
"flex h-auto min-h-10 w-full items-center justify-between rounded-md border bg-inherit p-1 hover:bg-inherit [&_svg]:pointer-events-auto",
className,
)}
>
{selectedValues.length > 0 ? (
<div className="flex w-full items-center justify-between">
<div className="flex flex-wrap items-center">
{selectedValues.slice(0, maxCount).map((value) => {
const option = options.find((o) => o === value);
return (
<Badge
key={value}
className={cn(
isAnimating ? "animate-bounce" : "",
multiSelectVariants({ variant }),
)}
style={{ animationDuration: `${animation}s` }}
>
{option}
<XCircle
className="ml-2 h-4 w-4 cursor-pointer"
onClick={(event) => {
event.stopPropagation();
toggleOption(value);
}}
/>
</Badge>
);
})}
{selectedValues.length > maxCount && (
<Badge
className={cn(
"border-foreground/1 bg-transparent text-foreground hover:bg-transparent",
isAnimating ? "animate-bounce" : "",
multiSelectVariants({ variant }),
)}
style={{ animationDuration: `${animation}s` }}
>
{`+ ${selectedValues.length - maxCount} more`}
<XCircle
className="ml-2 h-4 w-4 cursor-pointer"
onClick={(event) => {
event.stopPropagation();
clearExtraOptions();
}}
/>
</Badge>
)}
</div>
<div className="flex items-center justify-between">
<XIcon
className="mx-2 h-4 cursor-pointer text-muted-foreground"
onClick={(event) => {
event.stopPropagation();
handleClear();
}}
/>
<Separator
orientation="vertical"
className="flex h-full min-h-6"
/>
<ChevronDown className="mx-2 h-4 cursor-pointer text-muted-foreground" />
</div>
</div>
) : (
<div className="mx-auto flex w-full items-center justify-between">
<span className="mx-3 text-sm text-muted-foreground">
{placeholder}
</span>
<ChevronDown className="mx-2 h-4 cursor-pointer text-muted-foreground" />
</div>
)}
</Button>
</PopoverTrigger>
<PopoverContent
className="w-auto p-0"
align="start"
onEscapeKeyDown={() => setIsPopoverOpen(false)}
>
<Command>
<CommandInput
placeholder="Search..."
onKeyDown={handleInputKeyDown}
/>
<CommandList>
<CommandEmpty>No results found.</CommandEmpty>
<CommandGroup>
<CommandItem
key="all"
onSelect={toggleAll}
className="cursor-pointer"
>
<div
className={cn(
"mr-2 flex h-4 w-4 items-center justify-center rounded-sm border border-primary",
selectedValues.length === options.length
? "bg-primary text-primary-foreground"
: "opacity-50 [&_svg]:invisible",
)}
>
<CheckIcon className="h-4 w-4" />
</div>
<span>(Select All)</span>
</CommandItem>
{options.map((option) => {
const isSelected = selectedValues.includes(option);
return (
<CommandItem
key={option}
onSelect={() => toggleOption(option)}
className="cursor-pointer"
>
<div
className={cn(
"mr-2 flex h-4 w-4 items-center justify-center rounded-sm border border-primary",
isSelected
? "bg-primary text-primary-foreground"
: "opacity-50 [&_svg]:invisible",
)}
>
<CheckIcon className="h-4 w-4" />
</div>
<span>{option}</span>
</CommandItem>
);
})}
</CommandGroup>
<CommandSeparator />
<CommandGroup>
<div className="flex items-center justify-between">
{selectedValues.length > 0 && (
<>
<CommandItem
onSelect={handleClear}
className="flex-1 cursor-pointer justify-center"
>
Clear
</CommandItem>
<Separator
orientation="vertical"
className="flex h-full min-h-6"
/>
</>
)}
<CommandItem
onSelect={() => setIsPopoverOpen(false)}
className="max-w-full flex-1 cursor-pointer justify-center"
>
Close
</CommandItem>
</div>
</CommandGroup>
</CommandList>
</Command>
</PopoverContent>
</Popover>
);
},
);
MultiSelect.displayName = "MultiSelect";

View File

@ -41,7 +41,7 @@ export type BlockIOSubSchema =
| BlockIOSimpleTypeSubSchema
| BlockIOCombinedTypeSubSchema;
type BlockIOSimpleTypeSubSchema =
export type BlockIOSimpleTypeSubSchema =
| BlockIOObjectSubSchema
| BlockIOCredentialsSubSchema
| BlockIOKVSubSchema
@ -76,6 +76,7 @@ export type BlockIOKVSubSchema = BlockIOSubSchemaMeta & {
export type BlockIOArraySubSchema = BlockIOSubSchemaMeta & {
type: "array";
items?: BlockIOSimpleTypeSubSchema;
isMultiSelect?: boolean;
default?: Array<string>;
};
@ -125,6 +126,7 @@ export const PROVIDER_NAMES = {
UNREAL_SPEECH: "unreal_speech",
EXA: "exa",
HUBSPOT: "hubspot",
TWITTER: "twitter",
} as const;
// --8<-- [end:BlockIOCredentialsSubSchema]