mirror of
https://github.com/Significant-Gravitas/AutoGPT.git
synced 2026-01-21 04:57:58 -05:00
Compare commits
24 Commits
testing-cl
...
feature/tw
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6c94676e76 | ||
|
|
694b9df9da | ||
|
|
3c264f162c | ||
|
|
ce591cefda | ||
|
|
75ba8c2e83 | ||
|
|
ebd69374a6 | ||
|
|
ea13d970ab | ||
|
|
3cc0be9234 | ||
|
|
36af4a53f8 | ||
|
|
431a87ab6d | ||
|
|
d6297c43fd | ||
|
|
023c5b9a17 | ||
|
|
3344b7f51e | ||
|
|
159af2b89a | ||
|
|
67b17475b1 | ||
|
|
2968453f18 | ||
|
|
db57cecb1f | ||
|
|
1678a1c69c | ||
|
|
49a24f73b7 | ||
|
|
794dd4f59c | ||
|
|
3fb3099814 | ||
|
|
01185c55a1 | ||
|
|
e9e7f28cad | ||
|
|
ca26b298b6 |
@@ -0,0 +1,76 @@
|
|||||||
|
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)
|
||||||
@@ -58,6 +58,11 @@ GITHUB_CLIENT_SECRET=
|
|||||||
GOOGLE_CLIENT_ID=
|
GOOGLE_CLIENT_ID=
|
||||||
GOOGLE_CLIENT_SECRET=
|
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 ===== ##
|
## ===== OPTIONAL API KEYS ===== ##
|
||||||
|
|
||||||
# LLM
|
# LLM
|
||||||
|
|||||||
60
autogpt_platform/backend/backend/blocks/twitter/_auth.py
Normal file
60
autogpt_platform/backend/backend/blocks/twitter/_auth.py
Normal file
@@ -0,0 +1,60 @@
|
|||||||
|
from typing import Literal
|
||||||
|
|
||||||
|
from pydantic import SecretStr
|
||||||
|
|
||||||
|
from backend.data.model import (
|
||||||
|
CredentialsField,
|
||||||
|
CredentialsMetaInput,
|
||||||
|
OAuth2Credentials,
|
||||||
|
ProviderName,
|
||||||
|
)
|
||||||
|
from backend.integrations.oauth.twitter import TwitterOAuthHandler
|
||||||
|
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[ProviderName.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(
|
||||||
|
# required_scopes=set(scopes),
|
||||||
|
required_scopes=set(TwitterOAuthHandler.DEFAULT_SCOPES + 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,
|
||||||
|
}
|
||||||
418
autogpt_platform/backend/backend/blocks/twitter/_builders.py
Normal file
418
autogpt_platform/backend/backend/blocks/twitter/_builders.py
Normal file
@@ -0,0 +1,418 @@
|
|||||||
|
from datetime import datetime
|
||||||
|
from typing import Any, Dict
|
||||||
|
|
||||||
|
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,
|
||||||
|
)
|
||||||
|
from backend.blocks.twitter._types import ( # DMEventFieldFilter,
|
||||||
|
DMEventExpansionFilter,
|
||||||
|
DMEventTypeFilter,
|
||||||
|
DMMediaFieldFilter,
|
||||||
|
DMTweetFieldFilter,
|
||||||
|
ExpansionFilter,
|
||||||
|
ListExpansionsFilter,
|
||||||
|
ListFieldsFilter,
|
||||||
|
SpaceExpansionsFilter,
|
||||||
|
SpaceFieldsFilter,
|
||||||
|
TweetFieldsFilter,
|
||||||
|
TweetMediaFieldsFilter,
|
||||||
|
TweetPlaceFieldsFilter,
|
||||||
|
TweetPollFieldsFilter,
|
||||||
|
TweetReplySettingsFilter,
|
||||||
|
TweetUserFieldsFilter,
|
||||||
|
UserExpansionsFilter,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
# Common Builder
|
||||||
|
class TweetExpansionsBuilder:
|
||||||
|
def __init__(self, param: Dict[str, Any]):
|
||||||
|
self.params: Dict[str, Any] = param
|
||||||
|
|
||||||
|
def add_expansions(self, expansions: ExpansionFilter | None):
|
||||||
|
if expansions:
|
||||||
|
filtered_expansions = [
|
||||||
|
name for name, value in expansions.dict().items() if value is True
|
||||||
|
]
|
||||||
|
|
||||||
|
if filtered_expansions:
|
||||||
|
self.params["expansions"] = ",".join(
|
||||||
|
[get_backend_expansion(exp) for exp in filtered_expansions]
|
||||||
|
)
|
||||||
|
|
||||||
|
return self
|
||||||
|
|
||||||
|
def add_media_fields(self, media_fields: TweetMediaFieldsFilter | None):
|
||||||
|
if media_fields:
|
||||||
|
filtered_fields = [
|
||||||
|
name for name, value in media_fields.dict().items() if value is True
|
||||||
|
]
|
||||||
|
if filtered_fields:
|
||||||
|
self.params["media.fields"] = ",".join(
|
||||||
|
[get_backend_media_field(field) for field in filtered_fields]
|
||||||
|
)
|
||||||
|
return self
|
||||||
|
|
||||||
|
def add_place_fields(self, place_fields: TweetPlaceFieldsFilter | None):
|
||||||
|
if place_fields:
|
||||||
|
filtered_fields = [
|
||||||
|
name for name, value in place_fields.dict().items() if value is True
|
||||||
|
]
|
||||||
|
if filtered_fields:
|
||||||
|
self.params["place.fields"] = ",".join(
|
||||||
|
[get_backend_place_field(field) for field in filtered_fields]
|
||||||
|
)
|
||||||
|
return self
|
||||||
|
|
||||||
|
def add_poll_fields(self, poll_fields: TweetPollFieldsFilter | None):
|
||||||
|
if poll_fields:
|
||||||
|
filtered_fields = [
|
||||||
|
name for name, value in poll_fields.dict().items() if value is True
|
||||||
|
]
|
||||||
|
if filtered_fields:
|
||||||
|
self.params["poll.fields"] = ",".join(
|
||||||
|
[get_backend_poll_field(field) for field in filtered_fields]
|
||||||
|
)
|
||||||
|
return self
|
||||||
|
|
||||||
|
def add_tweet_fields(self, tweet_fields: TweetFieldsFilter | None):
|
||||||
|
if tweet_fields:
|
||||||
|
filtered_fields = [
|
||||||
|
name for name, value in tweet_fields.dict().items() if value is True
|
||||||
|
]
|
||||||
|
if filtered_fields:
|
||||||
|
self.params["tweet.fields"] = ",".join(
|
||||||
|
[get_backend_field(field) for field in filtered_fields]
|
||||||
|
)
|
||||||
|
return self
|
||||||
|
|
||||||
|
def add_user_fields(self, user_fields: TweetUserFieldsFilter | None):
|
||||||
|
if user_fields:
|
||||||
|
filtered_fields = [
|
||||||
|
name for name, value in user_fields.dict().items() if value is True
|
||||||
|
]
|
||||||
|
if filtered_fields:
|
||||||
|
self.params["user.fields"] = ",".join(
|
||||||
|
[get_backend_user_field(field) for field in filtered_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: UserExpansionsFilter | None):
|
||||||
|
if expansions:
|
||||||
|
filtered_expansions = [
|
||||||
|
name for name, value in expansions.dict().items() if value is True
|
||||||
|
]
|
||||||
|
if filtered_expansions:
|
||||||
|
self.params["expansions"] = ",".join(filtered_expansions)
|
||||||
|
return self
|
||||||
|
|
||||||
|
def add_tweet_fields(self, tweet_fields: TweetFieldsFilter | None):
|
||||||
|
if tweet_fields:
|
||||||
|
filtered_fields = [
|
||||||
|
name for name, value in tweet_fields.dict().items() if value is True
|
||||||
|
]
|
||||||
|
if filtered_fields:
|
||||||
|
self.params["tweet.fields"] = ",".join(
|
||||||
|
[get_backend_field(field) for field in filtered_fields]
|
||||||
|
)
|
||||||
|
return self
|
||||||
|
|
||||||
|
def add_user_fields(self, user_fields: TweetUserFieldsFilter | None):
|
||||||
|
if user_fields:
|
||||||
|
filtered_fields = [
|
||||||
|
name for name, value in user_fields.dict().items() if value is True
|
||||||
|
]
|
||||||
|
if filtered_fields:
|
||||||
|
self.params["user.fields"] = ",".join(
|
||||||
|
[get_backend_user_field(field) for field in filtered_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: ListExpansionsFilter | None):
|
||||||
|
if expansions:
|
||||||
|
filtered_expansions = [
|
||||||
|
name for name, value in expansions.dict().items() if value is True
|
||||||
|
]
|
||||||
|
if filtered_expansions:
|
||||||
|
self.params["expansions"] = ",".join(
|
||||||
|
[get_backend_list_expansion(exp) for exp in filtered_expansions]
|
||||||
|
)
|
||||||
|
return self
|
||||||
|
|
||||||
|
def add_list_fields(self, list_fields: ListFieldsFilter | None):
|
||||||
|
if list_fields:
|
||||||
|
filtered_fields = [
|
||||||
|
name for name, value in list_fields.dict().items() if value is True
|
||||||
|
]
|
||||||
|
if filtered_fields:
|
||||||
|
self.params["list.fields"] = ",".join(
|
||||||
|
[get_backend_list_field(field) for field in filtered_fields]
|
||||||
|
)
|
||||||
|
return self
|
||||||
|
|
||||||
|
def add_user_fields(self, user_fields: TweetUserFieldsFilter | None):
|
||||||
|
if user_fields:
|
||||||
|
filtered_fields = [
|
||||||
|
name for name, value in user_fields.dict().items() if value is True
|
||||||
|
]
|
||||||
|
if filtered_fields:
|
||||||
|
self.params["user.fields"] = ",".join(
|
||||||
|
[get_backend_user_field(field) for field in filtered_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: SpaceExpansionsFilter | None):
|
||||||
|
if expansions:
|
||||||
|
filtered_expansions = [
|
||||||
|
name for name, value in expansions.dict().items() if value is True
|
||||||
|
]
|
||||||
|
if filtered_expansions:
|
||||||
|
self.params["expansions"] = ",".join(
|
||||||
|
[get_backend_space_expansion(exp) for exp in filtered_expansions]
|
||||||
|
)
|
||||||
|
return self
|
||||||
|
|
||||||
|
def add_space_fields(self, space_fields: SpaceFieldsFilter | None):
|
||||||
|
if space_fields:
|
||||||
|
filtered_fields = [
|
||||||
|
name for name, value in space_fields.dict().items() if value is True
|
||||||
|
]
|
||||||
|
if filtered_fields:
|
||||||
|
self.params["space.fields"] = ",".join(
|
||||||
|
[get_backend_space_field(field) for field in filtered_fields]
|
||||||
|
)
|
||||||
|
return self
|
||||||
|
|
||||||
|
def add_user_fields(self, user_fields: TweetUserFieldsFilter | None):
|
||||||
|
if user_fields:
|
||||||
|
filtered_fields = [
|
||||||
|
name for name, value in user_fields.dict().items() if value is True
|
||||||
|
]
|
||||||
|
if filtered_fields:
|
||||||
|
self.params["user.fields"] = ",".join(
|
||||||
|
[get_backend_user_field(field) for field in filtered_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: datetime | None):
|
||||||
|
if start_time:
|
||||||
|
self.params["start_time"] = start_time
|
||||||
|
return self
|
||||||
|
|
||||||
|
def add_end_time(self, end_time: datetime | None):
|
||||||
|
if end_time:
|
||||||
|
self.params["end_time"] = end_time
|
||||||
|
return self
|
||||||
|
|
||||||
|
def add_since_id(self, since_id: str | None):
|
||||||
|
if since_id:
|
||||||
|
self.params["since_id"] = since_id
|
||||||
|
return self
|
||||||
|
|
||||||
|
def add_until_id(self, until_id: str | None):
|
||||||
|
if until_id:
|
||||||
|
self.params["until_id"] = until_id
|
||||||
|
return self
|
||||||
|
|
||||||
|
def add_sort_order(self, sort_order: str | None):
|
||||||
|
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: DMEventExpansionFilter):
|
||||||
|
if expansions:
|
||||||
|
filtered_expansions = [
|
||||||
|
name for name, value in expansions.dict().items() if value is True
|
||||||
|
]
|
||||||
|
if filtered_expansions:
|
||||||
|
self.params["expansions"] = ",".join(filtered_expansions)
|
||||||
|
return self
|
||||||
|
|
||||||
|
def add_event_types(self, event_types: DMEventTypeFilter):
|
||||||
|
if event_types:
|
||||||
|
filtered_types = [
|
||||||
|
name for name, value in event_types.dict().items() if value is True
|
||||||
|
]
|
||||||
|
if filtered_types:
|
||||||
|
self.params["event_types"] = ",".join(filtered_types)
|
||||||
|
return self
|
||||||
|
|
||||||
|
def add_media_fields(self, media_fields: DMMediaFieldFilter):
|
||||||
|
if media_fields:
|
||||||
|
filtered_fields = [
|
||||||
|
name for name, value in media_fields.dict().items() if value is True
|
||||||
|
]
|
||||||
|
if filtered_fields:
|
||||||
|
self.params["media.fields"] = ",".join(filtered_fields)
|
||||||
|
return self
|
||||||
|
|
||||||
|
def add_tweet_fields(self, tweet_fields: DMTweetFieldFilter):
|
||||||
|
if tweet_fields:
|
||||||
|
filtered_fields = [
|
||||||
|
name for name, value in tweet_fields.dict().items() if value is True
|
||||||
|
]
|
||||||
|
if filtered_fields:
|
||||||
|
self.params["tweet.fields"] = ",".join(filtered_fields)
|
||||||
|
return self
|
||||||
|
|
||||||
|
def add_user_fields(self, user_fields: TweetUserFieldsFilter):
|
||||||
|
if user_fields:
|
||||||
|
filtered_fields = [
|
||||||
|
name for name, value in user_fields.dict().items() if value is True
|
||||||
|
]
|
||||||
|
if filtered_fields:
|
||||||
|
self.params["user.fields"] = ",".join(filtered_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 | None):
|
||||||
|
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: TweetReplySettingsFilter,
|
||||||
|
):
|
||||||
|
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.All_Users:
|
||||||
|
self.params["reply_settings"] = None
|
||||||
|
elif settings.Following_Users_Only:
|
||||||
|
self.params["reply_settings"] = "following"
|
||||||
|
elif settings.Mentioned_Users_Only:
|
||||||
|
self.params["reply_settings"] = "mentionedUsers"
|
||||||
|
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
|
||||||
234
autogpt_platform/backend/backend/blocks/twitter/_mappers.py
Normal file
234
autogpt_platform/backend/backend/blocks/twitter/_mappers.py
Normal file
@@ -0,0 +1,234 @@
|
|||||||
|
# -------------- Tweets -----------------
|
||||||
|
|
||||||
|
# Tweet Expansions
|
||||||
|
EXPANSION_FRONTEND_TO_BACKEND_MAPPING = {
|
||||||
|
"Poll_IDs": "attachments.poll_ids",
|
||||||
|
"Media_Keys": "attachments.media_keys",
|
||||||
|
"Author_User_ID": "author_id",
|
||||||
|
"Edit_History_Tweet_IDs": "edit_history_tweet_ids",
|
||||||
|
"Mentioned_Usernames": "entities.mentions.username",
|
||||||
|
"Place_ID": "geo.place_id",
|
||||||
|
"Reply_To_User_ID": "in_reply_to_user_id",
|
||||||
|
"Referenced_Tweet_ID": "referenced_tweets.id",
|
||||||
|
"Referenced_Tweet_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_Only": "mentionedUsers",
|
||||||
|
"Following_Users_Only": "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 = {
|
||||||
|
"Account_Creation_Date": "created_at",
|
||||||
|
"User_Bio": "description",
|
||||||
|
"User_Entities": "entities",
|
||||||
|
"User_ID": "id",
|
||||||
|
"User_Location": "location",
|
||||||
|
"Latest_Tweet_ID": "most_recent_tweet_id",
|
||||||
|
"Display_Name": "name",
|
||||||
|
"Pinned_Tweet_ID": "pinned_tweet_id",
|
||||||
|
"Profile_Picture_URL": "profile_image_url",
|
||||||
|
"Is_Protected_Account": "protected",
|
||||||
|
"Account_Statistics": "public_metrics",
|
||||||
|
"Profile_URL": "url",
|
||||||
|
"Username": "username",
|
||||||
|
"Is_Verified": "verified",
|
||||||
|
"Verification_Type": "verified_type",
|
||||||
|
"Content_Withholding_Info": "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 = {
|
||||||
|
"Tweet_Attachments": "attachments",
|
||||||
|
"Author_ID": "author_id",
|
||||||
|
"Context_Annotations": "context_annotations",
|
||||||
|
"Conversation_ID": "conversation_id",
|
||||||
|
"Creation_Time": "created_at",
|
||||||
|
"Edit_Controls": "edit_controls",
|
||||||
|
"Tweet_Entities": "entities",
|
||||||
|
"Geographic_Location": "geo",
|
||||||
|
"Tweet_ID": "id",
|
||||||
|
"Reply_To_User_ID": "in_reply_to_user_id",
|
||||||
|
"Language": "lang",
|
||||||
|
"Public_Metrics": "public_metrics",
|
||||||
|
"Sensitive_Content_Flag": "possibly_sensitive",
|
||||||
|
"Referenced_Tweets": "referenced_tweets",
|
||||||
|
"Reply_Settings": "reply_settings",
|
||||||
|
"Tweet_Source": "source",
|
||||||
|
"Tweet_Text": "text",
|
||||||
|
"Withheld_Content": "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",
|
||||||
|
"Poll_ID": "id",
|
||||||
|
"Poll_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_Places": "contained_within",
|
||||||
|
"Country": "country",
|
||||||
|
"Country_Code": "country_code",
|
||||||
|
"Full_Location_Name": "full_name",
|
||||||
|
"Geographic_Coordinates": "geo",
|
||||||
|
"Place_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_in_Milliseconds": "duration_ms",
|
||||||
|
"Height": "height",
|
||||||
|
"Media_Key": "media_key",
|
||||||
|
"Preview_Image_URL": "preview_image_url",
|
||||||
|
"Media_Type": "type",
|
||||||
|
"Media_URL": "url",
|
||||||
|
"Width": "width",
|
||||||
|
"Public_Metrics": "public_metrics",
|
||||||
|
"Non_Public_Metrics": "non_public_metrics",
|
||||||
|
"Organic_Metrics": "organic_metrics",
|
||||||
|
"Promoted_Metrics": "promoted_metrics",
|
||||||
|
"Alternative_Text": "alt_text",
|
||||||
|
"Media_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_Users": "invited_user_ids",
|
||||||
|
"Speakers": "speaker_ids",
|
||||||
|
"Creator": "creator_id",
|
||||||
|
"Hosts": "host_ids",
|
||||||
|
"Topics": "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 = {
|
||||||
|
"Space_ID": "id",
|
||||||
|
"Space_State": "state",
|
||||||
|
"Creation_Time": "created_at",
|
||||||
|
"End_Time": "ended_at",
|
||||||
|
"Host_User_IDs": "host_ids",
|
||||||
|
"Language": "lang",
|
||||||
|
"Is_Ticketed": "is_ticketed",
|
||||||
|
"Invited_User_IDs": "invited_user_ids",
|
||||||
|
"Participant_Count": "participant_count",
|
||||||
|
"Subscriber_Count": "subscriber_count",
|
||||||
|
"Scheduled_Start_Time": "scheduled_start",
|
||||||
|
"Speaker_User_IDs": "speaker_ids",
|
||||||
|
"Start_Time": "started_at",
|
||||||
|
"Space_Title": "title",
|
||||||
|
"Topic_IDs": "topic_ids",
|
||||||
|
"Last_Updated_Time": "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 = {"List_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 = {
|
||||||
|
"List_ID": "id",
|
||||||
|
"List_Name": "name",
|
||||||
|
"Creation_Date": "created_at",
|
||||||
|
"Description": "description",
|
||||||
|
"Follower_Count": "follower_count",
|
||||||
|
"Member_Count": "member_count",
|
||||||
|
"Is_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
|
||||||
@@ -0,0 +1,76 @@
|
|||||||
|
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
|
||||||
443
autogpt_platform/backend/backend/blocks/twitter/_types.py
Normal file
443
autogpt_platform/backend/backend/blocks/twitter/_types.py
Normal file
@@ -0,0 +1,443 @@
|
|||||||
|
from datetime import datetime
|
||||||
|
from enum import Enum
|
||||||
|
|
||||||
|
from pydantic import BaseModel
|
||||||
|
|
||||||
|
from backend.data.block import BlockSchema
|
||||||
|
from backend.data.model import SchemaField
|
||||||
|
|
||||||
|
# -------------- Tweets -----------------
|
||||||
|
|
||||||
|
|
||||||
|
class TweetReplySettingsFilter(BaseModel):
|
||||||
|
Mentioned_Users_Only: bool = False
|
||||||
|
Following_Users_Only: bool = False
|
||||||
|
All_Users: bool = False
|
||||||
|
|
||||||
|
|
||||||
|
class TweetUserFieldsFilter(BaseModel):
|
||||||
|
Account_Creation_Date: bool = False
|
||||||
|
User_Bio: bool = False
|
||||||
|
User_Entities: bool = False
|
||||||
|
User_ID: bool = False
|
||||||
|
User_Location: bool = False
|
||||||
|
Latest_Tweet_ID: bool = False
|
||||||
|
Display_Name: bool = False
|
||||||
|
Pinned_Tweet_ID: bool = False
|
||||||
|
Profile_Picture_URL: bool = False
|
||||||
|
Is_Protected_Account: bool = False
|
||||||
|
Account_Statistics: bool = False
|
||||||
|
Profile_URL: bool = False
|
||||||
|
Username: bool = False
|
||||||
|
Is_Verified: bool = False
|
||||||
|
Verification_Type: bool = False
|
||||||
|
Content_Withholding_Info: bool = False
|
||||||
|
|
||||||
|
|
||||||
|
class TweetFieldsFilter(BaseModel):
|
||||||
|
Tweet_Attachments: bool = False
|
||||||
|
Author_ID: bool = False
|
||||||
|
Context_Annotations: bool = False
|
||||||
|
Conversation_ID: bool = False
|
||||||
|
Creation_Time: bool = False
|
||||||
|
Edit_Controls: bool = False
|
||||||
|
Tweet_Entities: bool = False
|
||||||
|
Geographic_Location: bool = False
|
||||||
|
Tweet_ID: bool = False
|
||||||
|
Reply_To_User_ID: bool = False
|
||||||
|
Language: bool = False
|
||||||
|
Public_Metrics: bool = False
|
||||||
|
Sensitive_Content_Flag: bool = False
|
||||||
|
Referenced_Tweets: bool = False
|
||||||
|
Reply_Settings: bool = False
|
||||||
|
Tweet_Source: bool = False
|
||||||
|
Tweet_Text: bool = False
|
||||||
|
Withheld_Content: bool = False
|
||||||
|
|
||||||
|
|
||||||
|
class PersonalTweetFieldsFilter(BaseModel):
|
||||||
|
attachments: bool = False
|
||||||
|
author_id: bool = False
|
||||||
|
context_annotations: bool = False
|
||||||
|
conversation_id: bool = False
|
||||||
|
created_at: bool = False
|
||||||
|
edit_controls: bool = False
|
||||||
|
entities: bool = False
|
||||||
|
geo: bool = False
|
||||||
|
id: bool = False
|
||||||
|
in_reply_to_user_id: bool = False
|
||||||
|
lang: bool = False
|
||||||
|
non_public_metrics: bool = False
|
||||||
|
public_metrics: bool = False
|
||||||
|
organic_metrics: bool = False
|
||||||
|
promoted_metrics: bool = False
|
||||||
|
possibly_sensitive: bool = False
|
||||||
|
referenced_tweets: bool = False
|
||||||
|
reply_settings: bool = False
|
||||||
|
source: bool = False
|
||||||
|
text: bool = False
|
||||||
|
withheld: bool = False
|
||||||
|
|
||||||
|
|
||||||
|
class TweetPollFieldsFilter(BaseModel):
|
||||||
|
Duration_Minutes: bool = False
|
||||||
|
End_DateTime: bool = False
|
||||||
|
Poll_ID: bool = False
|
||||||
|
Poll_Options: bool = False
|
||||||
|
Voting_Status: bool = False
|
||||||
|
|
||||||
|
|
||||||
|
class TweetPlaceFieldsFilter(BaseModel):
|
||||||
|
Contained_Within_Places: bool = False
|
||||||
|
Country: bool = False
|
||||||
|
Country_Code: bool = False
|
||||||
|
Full_Location_Name: bool = False
|
||||||
|
Geographic_Coordinates: bool = False
|
||||||
|
Place_ID: bool = False
|
||||||
|
Place_Name: bool = False
|
||||||
|
Place_Type: bool = False
|
||||||
|
|
||||||
|
|
||||||
|
class TweetMediaFieldsFilter(BaseModel):
|
||||||
|
Duration_in_Milliseconds: bool = False
|
||||||
|
Height: bool = False
|
||||||
|
Media_Key: bool = False
|
||||||
|
Preview_Image_URL: bool = False
|
||||||
|
Media_Type: bool = False
|
||||||
|
Media_URL: bool = False
|
||||||
|
Width: bool = False
|
||||||
|
Public_Metrics: bool = False
|
||||||
|
Non_Public_Metrics: bool = False
|
||||||
|
Organic_Metrics: bool = False
|
||||||
|
Promoted_Metrics: bool = False
|
||||||
|
Alternative_Text: bool = False
|
||||||
|
Media_Variants: bool = False
|
||||||
|
|
||||||
|
|
||||||
|
class ExpansionFilter(BaseModel):
|
||||||
|
Poll_IDs: bool = False
|
||||||
|
Media_Keys: bool = False
|
||||||
|
Author_User_ID: bool = False
|
||||||
|
Edit_History_Tweet_IDs: bool = False
|
||||||
|
Mentioned_Usernames: bool = False
|
||||||
|
Place_ID: bool = False
|
||||||
|
Reply_To_User_ID: bool = False
|
||||||
|
Referenced_Tweet_ID: bool = False
|
||||||
|
Referenced_Tweet_Author_ID: bool = False
|
||||||
|
|
||||||
|
|
||||||
|
class TweetExcludesFilter(BaseModel):
|
||||||
|
retweets: bool = False
|
||||||
|
replies: bool = False
|
||||||
|
|
||||||
|
|
||||||
|
# -------------- Users -----------------
|
||||||
|
|
||||||
|
|
||||||
|
class UserExpansionsFilter(BaseModel):
|
||||||
|
pinned_tweet_id: bool = False
|
||||||
|
|
||||||
|
|
||||||
|
# -------------- DM's' -----------------
|
||||||
|
|
||||||
|
|
||||||
|
class DMEventFieldFilter(BaseModel):
|
||||||
|
id: bool = False
|
||||||
|
text: bool = False
|
||||||
|
event_type: bool = False
|
||||||
|
created_at: bool = False
|
||||||
|
dm_conversation_id: bool = False
|
||||||
|
sender_id: bool = False
|
||||||
|
participant_ids: bool = False
|
||||||
|
referenced_tweets: bool = False
|
||||||
|
attachments: bool = False
|
||||||
|
|
||||||
|
|
||||||
|
class DMEventTypeFilter(BaseModel):
|
||||||
|
MessageCreate: bool = False
|
||||||
|
ParticipantsJoin: bool = False
|
||||||
|
ParticipantsLeave: bool = False
|
||||||
|
|
||||||
|
|
||||||
|
class DMEventExpansionFilter(BaseModel):
|
||||||
|
attachments_media_keys: bool = False
|
||||||
|
referenced_tweets_id: bool = False
|
||||||
|
sender_id: bool = False
|
||||||
|
participant_ids: bool = False
|
||||||
|
|
||||||
|
|
||||||
|
class DMMediaFieldFilter(BaseModel):
|
||||||
|
duration_ms: bool = False
|
||||||
|
height: bool = False
|
||||||
|
media_key: bool = False
|
||||||
|
preview_image_url: bool = False
|
||||||
|
type: bool = False
|
||||||
|
url: bool = False
|
||||||
|
width: bool = False
|
||||||
|
public_metrics: bool = False
|
||||||
|
alt_text: bool = False
|
||||||
|
variants: bool = False
|
||||||
|
|
||||||
|
|
||||||
|
class DMTweetFieldFilter(BaseModel):
|
||||||
|
attachments: bool = False
|
||||||
|
author_id: bool = False
|
||||||
|
context_annotations: bool = False
|
||||||
|
conversation_id: bool = False
|
||||||
|
created_at: bool = False
|
||||||
|
edit_controls: bool = False
|
||||||
|
entities: bool = False
|
||||||
|
geo: bool = False
|
||||||
|
id: bool = False
|
||||||
|
in_reply_to_user_id: bool = False
|
||||||
|
lang: bool = False
|
||||||
|
public_metrics: bool = False
|
||||||
|
possibly_sensitive: bool = False
|
||||||
|
referenced_tweets: bool = False
|
||||||
|
reply_settings: bool = False
|
||||||
|
source: bool = False
|
||||||
|
text: bool = False
|
||||||
|
withheld: bool = False
|
||||||
|
|
||||||
|
|
||||||
|
# -------------- Spaces -----------------
|
||||||
|
|
||||||
|
|
||||||
|
class SpaceExpansionsFilter(BaseModel):
|
||||||
|
Invited_Users: bool = False
|
||||||
|
Speakers: bool = False
|
||||||
|
Creator: bool = False
|
||||||
|
Hosts: bool = False
|
||||||
|
Topics: bool = False
|
||||||
|
|
||||||
|
|
||||||
|
class SpaceFieldsFilter(BaseModel):
|
||||||
|
Space_ID: bool = False
|
||||||
|
Space_State: bool = False
|
||||||
|
Creation_Time: bool = False
|
||||||
|
End_Time: bool = False
|
||||||
|
Host_User_IDs: bool = False
|
||||||
|
Language: bool = False
|
||||||
|
Is_Ticketed: bool = False
|
||||||
|
Invited_User_IDs: bool = False
|
||||||
|
Participant_Count: bool = False
|
||||||
|
Subscriber_Count: bool = False
|
||||||
|
Scheduled_Start_Time: bool = False
|
||||||
|
Speaker_User_IDs: bool = False
|
||||||
|
Start_Time: bool = False
|
||||||
|
Space_Title: bool = False
|
||||||
|
Topic_IDs: bool = False
|
||||||
|
Last_Updated_Time: bool = False
|
||||||
|
|
||||||
|
|
||||||
|
class SpaceStatesFilter(str, Enum):
|
||||||
|
live = "live"
|
||||||
|
scheduled = "scheduled"
|
||||||
|
all = "all"
|
||||||
|
|
||||||
|
|
||||||
|
# -------------- List Expansions -----------------
|
||||||
|
|
||||||
|
|
||||||
|
class ListExpansionsFilter(BaseModel):
|
||||||
|
List_Owner_ID: bool = False
|
||||||
|
|
||||||
|
|
||||||
|
class ListFieldsFilter(BaseModel):
|
||||||
|
List_ID: bool = False
|
||||||
|
List_Name: bool = False
|
||||||
|
Creation_Date: bool = False
|
||||||
|
Description: bool = False
|
||||||
|
Follower_Count: bool = False
|
||||||
|
Member_Count: bool = False
|
||||||
|
Is_Private: bool = False
|
||||||
|
Owner_ID: bool = False
|
||||||
|
|
||||||
|
|
||||||
|
# --------- [Input Types] -------------
|
||||||
|
class TweetExpansionInputs(BlockSchema):
|
||||||
|
|
||||||
|
expansions: ExpansionFilter | None = 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",
|
||||||
|
placeholder="Pick the extra information you want to see",
|
||||||
|
default=None,
|
||||||
|
advanced=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
media_fields: TweetMediaFieldsFilter | None = 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.",
|
||||||
|
placeholder="Choose what media details you want to see",
|
||||||
|
default=None,
|
||||||
|
advanced=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
place_fields: TweetPlaceFieldsFilter | None = 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=None,
|
||||||
|
advanced=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
poll_fields: TweetPollFieldsFilter | None = 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=None,
|
||||||
|
advanced=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
tweet_fields: TweetFieldsFilter | None = 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=None,
|
||||||
|
advanced=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
user_fields: TweetUserFieldsFilter | None = 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=None,
|
||||||
|
advanced=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class DMEventExpansionInputs(BlockSchema):
|
||||||
|
expansions: DMEventExpansionFilter | None = SchemaField(
|
||||||
|
description="Select expansions to include related data objects in the 'includes' section.",
|
||||||
|
placeholder="Enter expansions",
|
||||||
|
default=None,
|
||||||
|
advanced=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
event_types: DMEventTypeFilter | None = SchemaField(
|
||||||
|
description="Select DM event types to include in the response.",
|
||||||
|
placeholder="Enter event types",
|
||||||
|
default=None,
|
||||||
|
advanced=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
media_fields: DMMediaFieldFilter | None = SchemaField(
|
||||||
|
description="Select media fields to include in the response (requires expansions=attachments.media_keys).",
|
||||||
|
placeholder="Enter media fields",
|
||||||
|
default=None,
|
||||||
|
advanced=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
tweet_fields: DMTweetFieldFilter | None = SchemaField(
|
||||||
|
description="Select tweet fields to include in the response (requires expansions=referenced_tweets.id).",
|
||||||
|
placeholder="Enter tweet fields",
|
||||||
|
default=None,
|
||||||
|
advanced=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
user_fields: TweetUserFieldsFilter | None = SchemaField(
|
||||||
|
description="Select user fields to include in the response (requires expansions=sender_id or participant_ids).",
|
||||||
|
placeholder="Enter user fields",
|
||||||
|
default=None,
|
||||||
|
advanced=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class UserExpansionInputs(BlockSchema):
|
||||||
|
expansions: UserExpansionsFilter | None = 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.",
|
||||||
|
placeholder="Select extra user information to include",
|
||||||
|
default=None,
|
||||||
|
advanced=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
tweet_fields: TweetFieldsFilter | None = 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=None,
|
||||||
|
advanced=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
user_fields: TweetUserFieldsFilter | None = 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=None,
|
||||||
|
advanced=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class SpaceExpansionInputs(BlockSchema):
|
||||||
|
expansions: SpaceExpansionsFilter | None = 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",
|
||||||
|
placeholder="Pick what extra information you want to see about the Space",
|
||||||
|
default=None,
|
||||||
|
advanced=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
space_fields: SpaceFieldsFilter | None = 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=SpaceFieldsFilter(Space_Title=True, Host_User_IDs=True),
|
||||||
|
advanced=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
user_fields: TweetUserFieldsFilter | None = 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=None,
|
||||||
|
advanced=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class ListExpansionInputs(BlockSchema):
|
||||||
|
expansions: ListExpansionsFilter | None = 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.",
|
||||||
|
placeholder="Pick what extra list information you want to see",
|
||||||
|
default=ListExpansionsFilter(List_Owner_ID=True),
|
||||||
|
advanced=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
user_fields: TweetUserFieldsFilter | None = 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=TweetUserFieldsFilter(User_ID=True, Username=True),
|
||||||
|
advanced=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
list_fields: ListFieldsFilter | None = 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=ListFieldsFilter(Owner_ID=True),
|
||||||
|
advanced=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class TweetTimeWindowInputs(BlockSchema):
|
||||||
|
start_time: datetime | None = SchemaField(
|
||||||
|
description="Start time in YYYY-MM-DDTHH:mm:ssZ format",
|
||||||
|
placeholder="Enter start time",
|
||||||
|
default=None,
|
||||||
|
advanced=False,
|
||||||
|
)
|
||||||
|
|
||||||
|
end_time: datetime | None = SchemaField(
|
||||||
|
description="End time in YYYY-MM-DDTHH:mm:ssZ format",
|
||||||
|
placeholder="Enter end time",
|
||||||
|
default=None,
|
||||||
|
advanced=False,
|
||||||
|
)
|
||||||
|
|
||||||
|
since_id: str | None = SchemaField(
|
||||||
|
description="Returns results with Tweet ID greater than this (more recent than), we give priority to since_id over start_time",
|
||||||
|
placeholder="Enter since ID",
|
||||||
|
default=None,
|
||||||
|
advanced=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
until_id: str | None = SchemaField(
|
||||||
|
description="Returns results with Tweet ID less than this (that is, older than), and used with since_id",
|
||||||
|
placeholder="Enter until ID",
|
||||||
|
default=None,
|
||||||
|
advanced=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
sort_order: str | None = SchemaField(
|
||||||
|
description="Order of returned tweets (recency or relevancy)",
|
||||||
|
placeholder="Enter sort order",
|
||||||
|
default=None,
|
||||||
|
advanced=True,
|
||||||
|
)
|
||||||
@@ -0,0 +1,201 @@
|
|||||||
|
# Todo : Add new Type support
|
||||||
|
|
||||||
|
# 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)
|
||||||
@@ -0,0 +1,260 @@
|
|||||||
|
# Todo : Add new Type support
|
||||||
|
|
||||||
|
# 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)
|
||||||
@@ -0,0 +1,470 @@
|
|||||||
|
# from typing import cast
|
||||||
|
import tweepy
|
||||||
|
|
||||||
|
from backend.blocks.twitter._auth import (
|
||||||
|
TEST_CREDENTIALS,
|
||||||
|
TEST_CREDENTIALS_INPUT,
|
||||||
|
TwitterCredentials,
|
||||||
|
TwitterCredentialsField,
|
||||||
|
TwitterCredentialsInput,
|
||||||
|
)
|
||||||
|
|
||||||
|
# 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.data.block import Block, BlockCategory, BlockOutput, BlockSchema
|
||||||
|
from backend.data.model import SchemaField
|
||||||
|
|
||||||
|
# from tweepy.client import Response
|
||||||
|
|
||||||
|
|
||||||
|
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),
|
||||||
|
],
|
||||||
|
test_mock={"unfollow_list": lambda *args, **kwargs: 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),
|
||||||
|
],
|
||||||
|
test_mock={"follow_list": lambda *args, **kwargs: True},
|
||||||
|
)
|
||||||
|
|
||||||
|
@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], There is a high possibility that we might get error in this
|
||||||
|
# Needs Type Input in this
|
||||||
|
|
||||||
|
# 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)
|
||||||
@@ -0,0 +1,340 @@
|
|||||||
|
from typing import cast
|
||||||
|
|
||||||
|
import tweepy
|
||||||
|
from tweepy.client import Response
|
||||||
|
|
||||||
|
from backend.blocks.twitter._auth import (
|
||||||
|
TEST_CREDENTIALS,
|
||||||
|
TEST_CREDENTIALS_INPUT,
|
||||||
|
TwitterCredentials,
|
||||||
|
TwitterCredentialsField,
|
||||||
|
TwitterCredentialsInput,
|
||||||
|
)
|
||||||
|
from backend.blocks.twitter._builders import ListExpansionsBuilder
|
||||||
|
from backend.blocks.twitter._serializer import (
|
||||||
|
IncludesSerializer,
|
||||||
|
ResponseDataSerializer,
|
||||||
|
)
|
||||||
|
from backend.blocks.twitter._types import (
|
||||||
|
ListExpansionInputs,
|
||||||
|
ListExpansionsFilter,
|
||||||
|
ListFieldsFilter,
|
||||||
|
TweetUserFieldsFilter,
|
||||||
|
)
|
||||||
|
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
|
||||||
|
|
||||||
|
|
||||||
|
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": None,
|
||||||
|
"list_fields": None,
|
||||||
|
"user_fields": None,
|
||||||
|
},
|
||||||
|
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"}),
|
||||||
|
],
|
||||||
|
test_mock={
|
||||||
|
"get_list": lambda *args, **kwargs: (
|
||||||
|
{"id": "84839422", "name": "Official Twitter Accounts"},
|
||||||
|
{},
|
||||||
|
{},
|
||||||
|
"2244994945",
|
||||||
|
"TwitterAPI",
|
||||||
|
)
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def get_list(
|
||||||
|
credentials: TwitterCredentials,
|
||||||
|
list_id: str,
|
||||||
|
expansions: ListExpansionsFilter | None,
|
||||||
|
user_fields: TweetUserFieldsFilter | None,
|
||||||
|
list_fields: ListFieldsFilter | None,
|
||||||
|
):
|
||||||
|
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 | None = 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": None,
|
||||||
|
"list_fields": None,
|
||||||
|
"user_fields": None,
|
||||||
|
},
|
||||||
|
test_credentials=TEST_CREDENTIALS,
|
||||||
|
test_output=[
|
||||||
|
("list_ids", ["84839422"]),
|
||||||
|
("list_names", ["Official Twitter Accounts"]),
|
||||||
|
("data", [{"id": "84839422", "name": "Official Twitter Accounts"}]),
|
||||||
|
],
|
||||||
|
test_mock={
|
||||||
|
"get_owned_lists": lambda *args, **kwargs: (
|
||||||
|
[{"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 | None,
|
||||||
|
expansions: ListExpansionsFilter | None,
|
||||||
|
user_fields: TweetUserFieldsFilter | None,
|
||||||
|
list_fields: ListFieldsFilter | None,
|
||||||
|
):
|
||||||
|
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)
|
||||||
@@ -0,0 +1,523 @@
|
|||||||
|
from typing import cast
|
||||||
|
|
||||||
|
import tweepy
|
||||||
|
from tweepy.client import Response
|
||||||
|
|
||||||
|
from backend.blocks.twitter._auth import (
|
||||||
|
TEST_CREDENTIALS,
|
||||||
|
TEST_CREDENTIALS_INPUT,
|
||||||
|
TwitterCredentials,
|
||||||
|
TwitterCredentialsField,
|
||||||
|
TwitterCredentialsInput,
|
||||||
|
)
|
||||||
|
from backend.blocks.twitter._builders import (
|
||||||
|
ListExpansionsBuilder,
|
||||||
|
UserExpansionsBuilder,
|
||||||
|
)
|
||||||
|
from backend.blocks.twitter._serializer import (
|
||||||
|
IncludesSerializer,
|
||||||
|
ResponseDataSerializer,
|
||||||
|
)
|
||||||
|
from backend.blocks.twitter._types import (
|
||||||
|
ListExpansionInputs,
|
||||||
|
ListExpansionsFilter,
|
||||||
|
ListFieldsFilter,
|
||||||
|
TweetFieldsFilter,
|
||||||
|
TweetUserFieldsFilter,
|
||||||
|
UserExpansionInputs,
|
||||||
|
UserExpansionsFilter,
|
||||||
|
)
|
||||||
|
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
|
||||||
|
|
||||||
|
|
||||||
|
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:
|
||||||
|
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:
|
||||||
|
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 | None = 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": 2,
|
||||||
|
"pagination_token": None,
|
||||||
|
"credentials": TEST_CREDENTIALS_INPUT,
|
||||||
|
"expansions": None,
|
||||||
|
"tweet_fields": None,
|
||||||
|
"user_fields": None,
|
||||||
|
},
|
||||||
|
test_credentials=TEST_CREDENTIALS,
|
||||||
|
test_output=[
|
||||||
|
("ids", ["12345", "67890"]),
|
||||||
|
("usernames", ["testuser1", "testuser2"]),
|
||||||
|
(
|
||||||
|
"data",
|
||||||
|
[
|
||||||
|
{"id": "12345", "username": "testuser1"},
|
||||||
|
{"id": "67890", "username": "testuser2"},
|
||||||
|
],
|
||||||
|
),
|
||||||
|
],
|
||||||
|
test_mock={
|
||||||
|
"get_list_members": lambda *args, **kwargs: (
|
||||||
|
["12345", "67890"],
|
||||||
|
["testuser1", "testuser2"],
|
||||||
|
[
|
||||||
|
{"id": "12345", "username": "testuser1"},
|
||||||
|
{"id": "67890", "username": "testuser2"},
|
||||||
|
],
|
||||||
|
{},
|
||||||
|
{},
|
||||||
|
None,
|
||||||
|
)
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def get_list_members(
|
||||||
|
credentials: TwitterCredentials,
|
||||||
|
list_id: str,
|
||||||
|
max_results: int,
|
||||||
|
pagination_token: str | None,
|
||||||
|
expansions: UserExpansionsFilter | None,
|
||||||
|
tweet_fields: TweetFieldsFilter | None,
|
||||||
|
user_fields: TweetUserFieldsFilter | None,
|
||||||
|
):
|
||||||
|
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 | None = 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": 1,
|
||||||
|
"pagination_token": None,
|
||||||
|
"credentials": TEST_CREDENTIALS_INPUT,
|
||||||
|
"expansions": None,
|
||||||
|
"list_fields": None,
|
||||||
|
"user_fields": None,
|
||||||
|
},
|
||||||
|
test_credentials=TEST_CREDENTIALS,
|
||||||
|
test_output=[
|
||||||
|
("list_ids", ["84839422"]),
|
||||||
|
("data", [{"id": "84839422"}]),
|
||||||
|
],
|
||||||
|
test_mock={
|
||||||
|
"get_list_memberships": lambda *args, **kwargs: (
|
||||||
|
[{"id": "84839422"}],
|
||||||
|
{},
|
||||||
|
{},
|
||||||
|
["84839422"],
|
||||||
|
None,
|
||||||
|
)
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def get_list_memberships(
|
||||||
|
credentials: TwitterCredentials,
|
||||||
|
user_id: str,
|
||||||
|
max_results: int,
|
||||||
|
pagination_token: str | None,
|
||||||
|
expansions: ListExpansionsFilter | None,
|
||||||
|
user_fields: TweetUserFieldsFilter | None,
|
||||||
|
list_fields: ListFieldsFilter | None,
|
||||||
|
):
|
||||||
|
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)
|
||||||
@@ -0,0 +1,215 @@
|
|||||||
|
from typing import cast
|
||||||
|
|
||||||
|
import tweepy
|
||||||
|
from tweepy.client import Response
|
||||||
|
|
||||||
|
from backend.blocks.twitter._auth import (
|
||||||
|
TEST_CREDENTIALS,
|
||||||
|
TEST_CREDENTIALS_INPUT,
|
||||||
|
TwitterCredentials,
|
||||||
|
TwitterCredentialsField,
|
||||||
|
TwitterCredentialsInput,
|
||||||
|
)
|
||||||
|
from backend.blocks.twitter._builders import TweetExpansionsBuilder
|
||||||
|
from backend.blocks.twitter._serializer import (
|
||||||
|
IncludesSerializer,
|
||||||
|
ResponseDataSerializer,
|
||||||
|
)
|
||||||
|
from backend.blocks.twitter._types import (
|
||||||
|
ExpansionFilter,
|
||||||
|
TweetExpansionInputs,
|
||||||
|
TweetFieldsFilter,
|
||||||
|
TweetMediaFieldsFilter,
|
||||||
|
TweetPlaceFieldsFilter,
|
||||||
|
TweetPollFieldsFilter,
|
||||||
|
TweetUserFieldsFilter,
|
||||||
|
)
|
||||||
|
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
|
||||||
|
|
||||||
|
|
||||||
|
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 | None = 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": 1,
|
||||||
|
"pagination_token": None,
|
||||||
|
"credentials": TEST_CREDENTIALS_INPUT,
|
||||||
|
"expansions": None,
|
||||||
|
"media_fields": None,
|
||||||
|
"place_fields": None,
|
||||||
|
"poll_fields": None,
|
||||||
|
"tweet_fields": None,
|
||||||
|
"user_fields": None,
|
||||||
|
},
|
||||||
|
test_credentials=TEST_CREDENTIALS,
|
||||||
|
test_output=[
|
||||||
|
("tweet_ids", ["1234567890"]),
|
||||||
|
("texts", ["Test tweet"]),
|
||||||
|
("data", [{"id": "1234567890", "text": "Test tweet"}]),
|
||||||
|
],
|
||||||
|
test_mock={
|
||||||
|
"get_list_tweets": lambda *args, **kwargs: (
|
||||||
|
[{"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 | None,
|
||||||
|
expansions: ExpansionFilter | None,
|
||||||
|
media_fields: TweetMediaFieldsFilter | None,
|
||||||
|
place_fields: TweetPlaceFieldsFilter | None,
|
||||||
|
poll_fields: TweetPollFieldsFilter | None,
|
||||||
|
tweet_fields: TweetFieldsFilter | None,
|
||||||
|
user_fields: TweetUserFieldsFilter | None,
|
||||||
|
):
|
||||||
|
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)
|
||||||
@@ -0,0 +1,275 @@
|
|||||||
|
from typing import cast
|
||||||
|
|
||||||
|
import tweepy
|
||||||
|
from tweepy.client import Response
|
||||||
|
|
||||||
|
from backend.blocks.twitter._auth import (
|
||||||
|
TEST_CREDENTIALS,
|
||||||
|
TEST_CREDENTIALS_INPUT,
|
||||||
|
TwitterCredentials,
|
||||||
|
TwitterCredentialsField,
|
||||||
|
TwitterCredentialsInput,
|
||||||
|
)
|
||||||
|
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
|
||||||
|
|
||||||
|
|
||||||
|
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:
|
||||||
|
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:
|
||||||
|
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: ("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,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
list_id = str(response.data["id"])
|
||||||
|
|
||||||
|
return list_id
|
||||||
|
|
||||||
|
except tweepy.TweepyException:
|
||||||
|
raise
|
||||||
|
except Exception:
|
||||||
|
raise
|
||||||
|
|
||||||
|
def run(
|
||||||
|
self,
|
||||||
|
input_data: Input,
|
||||||
|
*,
|
||||||
|
credentials: TwitterCredentials,
|
||||||
|
**kwargs,
|
||||||
|
) -> BlockOutput:
|
||||||
|
try:
|
||||||
|
list_id = self.create_list(
|
||||||
|
credentials, input_data.name, input_data.description, input_data.private
|
||||||
|
)
|
||||||
|
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)
|
||||||
@@ -0,0 +1,283 @@
|
|||||||
|
from typing import cast
|
||||||
|
|
||||||
|
import tweepy
|
||||||
|
from tweepy.client import Response
|
||||||
|
|
||||||
|
from backend.blocks.twitter._auth import (
|
||||||
|
TEST_CREDENTIALS,
|
||||||
|
TEST_CREDENTIALS_INPUT,
|
||||||
|
TwitterCredentials,
|
||||||
|
TwitterCredentialsField,
|
||||||
|
TwitterCredentialsInput,
|
||||||
|
)
|
||||||
|
from backend.blocks.twitter._builders import ListExpansionsBuilder
|
||||||
|
from backend.blocks.twitter._serializer import (
|
||||||
|
IncludesSerializer,
|
||||||
|
ResponseDataSerializer,
|
||||||
|
)
|
||||||
|
from backend.blocks.twitter._types import (
|
||||||
|
ListExpansionInputs,
|
||||||
|
ListExpansionsFilter,
|
||||||
|
ListFieldsFilter,
|
||||||
|
TweetUserFieldsFilter,
|
||||||
|
)
|
||||||
|
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
|
||||||
|
|
||||||
|
|
||||||
|
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:
|
||||||
|
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:
|
||||||
|
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": None,
|
||||||
|
"list_fields": None,
|
||||||
|
"user_fields": None,
|
||||||
|
"credentials": TEST_CREDENTIALS_INPUT,
|
||||||
|
},
|
||||||
|
test_credentials=TEST_CREDENTIALS,
|
||||||
|
test_output=[
|
||||||
|
("list_ids", ["84839422"]),
|
||||||
|
("list_names", ["Twitter List"]),
|
||||||
|
("data", [{"id": "84839422", "name": "Twitter List"}]),
|
||||||
|
],
|
||||||
|
test_mock={
|
||||||
|
"get_pinned_lists": lambda *args, **kwargs: (
|
||||||
|
[{"id": "84839422", "name": "Twitter List"}],
|
||||||
|
{},
|
||||||
|
{},
|
||||||
|
["84839422"],
|
||||||
|
["Twitter List"],
|
||||||
|
)
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def get_pinned_lists(
|
||||||
|
credentials: TwitterCredentials,
|
||||||
|
expansions: ListExpansionsFilter | None,
|
||||||
|
user_fields: TweetUserFieldsFilter | None,
|
||||||
|
list_fields: ListFieldsFilter | None,
|
||||||
|
):
|
||||||
|
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)
|
||||||
@@ -0,0 +1,195 @@
|
|||||||
|
from typing import cast
|
||||||
|
|
||||||
|
import tweepy
|
||||||
|
from tweepy.client import Response
|
||||||
|
|
||||||
|
from backend.blocks.twitter._auth import (
|
||||||
|
TEST_CREDENTIALS,
|
||||||
|
TEST_CREDENTIALS_INPUT,
|
||||||
|
TwitterCredentials,
|
||||||
|
TwitterCredentialsField,
|
||||||
|
TwitterCredentialsInput,
|
||||||
|
)
|
||||||
|
from backend.blocks.twitter._builders import SpaceExpansionsBuilder
|
||||||
|
from backend.blocks.twitter._serializer import (
|
||||||
|
IncludesSerializer,
|
||||||
|
ResponseDataSerializer,
|
||||||
|
)
|
||||||
|
from backend.blocks.twitter._types import (
|
||||||
|
SpaceExpansionInputs,
|
||||||
|
SpaceExpansionsFilter,
|
||||||
|
SpaceFieldsFilter,
|
||||||
|
SpaceStatesFilter,
|
||||||
|
TweetUserFieldsFilter,
|
||||||
|
)
|
||||||
|
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
|
||||||
|
|
||||||
|
|
||||||
|
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: SpaceStatesFilter = SchemaField(
|
||||||
|
description="Type of Spaces to return (live, scheduled, or all)",
|
||||||
|
placeholder="Enter state filter",
|
||||||
|
default=SpaceStatesFilter.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: list[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": 1,
|
||||||
|
"state": "live",
|
||||||
|
"credentials": TEST_CREDENTIALS_INPUT,
|
||||||
|
"expansions": None,
|
||||||
|
"space_fields": None,
|
||||||
|
"user_fields": None,
|
||||||
|
},
|
||||||
|
test_credentials=TEST_CREDENTIALS,
|
||||||
|
test_output=[
|
||||||
|
("ids", ["1234"]),
|
||||||
|
("titles", ["Tech Talk"]),
|
||||||
|
("host_ids", ["5678"]),
|
||||||
|
("data", [{"id": "1234", "title": "Tech Talk", "host_ids": ["5678"]}]),
|
||||||
|
],
|
||||||
|
test_mock={
|
||||||
|
"search_spaces": lambda *args, **kwargs: (
|
||||||
|
[{"id": "1234", "title": "Tech Talk", "host_ids": ["5678"]}],
|
||||||
|
{},
|
||||||
|
{},
|
||||||
|
["1234"],
|
||||||
|
["Tech Talk"],
|
||||||
|
["5678"],
|
||||||
|
None,
|
||||||
|
)
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def search_spaces(
|
||||||
|
credentials: TwitterCredentials,
|
||||||
|
query: str,
|
||||||
|
max_results: int,
|
||||||
|
state: SpaceStatesFilter,
|
||||||
|
expansions: SpaceExpansionsFilter | None,
|
||||||
|
space_fields: SpaceFieldsFilter | None,
|
||||||
|
user_fields: TweetUserFieldsFilter | None,
|
||||||
|
):
|
||||||
|
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)
|
||||||
@@ -0,0 +1,629 @@
|
|||||||
|
from typing import cast
|
||||||
|
|
||||||
|
import tweepy
|
||||||
|
from tweepy.client import Response
|
||||||
|
|
||||||
|
from backend.blocks.twitter._auth import (
|
||||||
|
TEST_CREDENTIALS,
|
||||||
|
TEST_CREDENTIALS_INPUT,
|
||||||
|
TwitterCredentials,
|
||||||
|
TwitterCredentialsField,
|
||||||
|
TwitterCredentialsInput,
|
||||||
|
)
|
||||||
|
from backend.blocks.twitter._builders import (
|
||||||
|
SpaceExpansionsBuilder,
|
||||||
|
TweetExpansionsBuilder,
|
||||||
|
UserExpansionsBuilder,
|
||||||
|
)
|
||||||
|
from backend.blocks.twitter._serializer import (
|
||||||
|
IncludesSerializer,
|
||||||
|
ResponseDataSerializer,
|
||||||
|
)
|
||||||
|
from backend.blocks.twitter._types import (
|
||||||
|
ExpansionFilter,
|
||||||
|
SpaceExpansionInputs,
|
||||||
|
SpaceExpansionsFilter,
|
||||||
|
SpaceFieldsFilter,
|
||||||
|
TweetExpansionInputs,
|
||||||
|
TweetFieldsFilter,
|
||||||
|
TweetMediaFieldsFilter,
|
||||||
|
TweetPlaceFieldsFilter,
|
||||||
|
TweetPollFieldsFilter,
|
||||||
|
TweetUserFieldsFilter,
|
||||||
|
UserExpansionInputs,
|
||||||
|
UserExpansionsFilter,
|
||||||
|
)
|
||||||
|
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
|
||||||
|
|
||||||
|
|
||||||
|
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": [],
|
||||||
|
"credentials": TEST_CREDENTIALS_INPUT,
|
||||||
|
"expansions": None,
|
||||||
|
"space_fields": None,
|
||||||
|
"user_fields": None,
|
||||||
|
},
|
||||||
|
test_credentials=TEST_CREDENTIALS,
|
||||||
|
test_output=[
|
||||||
|
("ids", ["1DXxyRYNejbKM"]),
|
||||||
|
("titles", ["Test Space"]),
|
||||||
|
(
|
||||||
|
"data",
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"id": "1DXxyRYNejbKM",
|
||||||
|
"title": "Test Space",
|
||||||
|
"host_id": "1234567",
|
||||||
|
}
|
||||||
|
],
|
||||||
|
),
|
||||||
|
],
|
||||||
|
test_mock={
|
||||||
|
"get_spaces": lambda *args, **kwargs: (
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"id": "1DXxyRYNejbKM",
|
||||||
|
"title": "Test Space",
|
||||||
|
"host_id": "1234567",
|
||||||
|
}
|
||||||
|
],
|
||||||
|
{},
|
||||||
|
["1DXxyRYNejbKM"],
|
||||||
|
["Test Space"],
|
||||||
|
)
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def get_spaces(
|
||||||
|
credentials: TwitterCredentials,
|
||||||
|
space_ids: list[str],
|
||||||
|
user_ids: list[str],
|
||||||
|
expansions: SpaceExpansionsFilter | None,
|
||||||
|
space_fields: SpaceFieldsFilter | None,
|
||||||
|
user_fields: TweetUserFieldsFilter | None,
|
||||||
|
):
|
||||||
|
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()
|
||||||
|
)
|
||||||
|
|
||||||
|
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": None,
|
||||||
|
"space_fields": None,
|
||||||
|
"user_fields": None,
|
||||||
|
},
|
||||||
|
test_credentials=TEST_CREDENTIALS,
|
||||||
|
test_output=[
|
||||||
|
("id", "1DXxyRYNejbKM"),
|
||||||
|
("title", "Test Space"),
|
||||||
|
("host_ids", ["1234567"]),
|
||||||
|
(
|
||||||
|
"data",
|
||||||
|
{
|
||||||
|
"id": "1DXxyRYNejbKM",
|
||||||
|
"title": "Test Space",
|
||||||
|
"host_ids": ["1234567"],
|
||||||
|
},
|
||||||
|
),
|
||||||
|
],
|
||||||
|
test_mock={
|
||||||
|
"get_space": lambda *args, **kwargs: (
|
||||||
|
{
|
||||||
|
"id": "1DXxyRYNejbKM",
|
||||||
|
"title": "Test Space",
|
||||||
|
"host_ids": ["1234567"],
|
||||||
|
},
|
||||||
|
{},
|
||||||
|
)
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def get_space(
|
||||||
|
credentials: TwitterCredentials,
|
||||||
|
space_id: str,
|
||||||
|
expansions: SpaceExpansionsFilter | None,
|
||||||
|
space_fields: SpaceFieldsFilter | None,
|
||||||
|
user_fields: TweetUserFieldsFilter | None,
|
||||||
|
):
|
||||||
|
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": None,
|
||||||
|
"user_fields": None,
|
||||||
|
},
|
||||||
|
test_credentials=TEST_CREDENTIALS,
|
||||||
|
test_output=[
|
||||||
|
("buyer_ids", ["2244994945"]),
|
||||||
|
("usernames", ["testuser"]),
|
||||||
|
(
|
||||||
|
"data",
|
||||||
|
[{"id": "2244994945", "username": "testuser", "name": "Test User"}],
|
||||||
|
),
|
||||||
|
],
|
||||||
|
test_mock={
|
||||||
|
"get_space_buyers": lambda *args, **kwargs: (
|
||||||
|
[{"id": "2244994945", "username": "testuser", "name": "Test User"}],
|
||||||
|
{},
|
||||||
|
["2244994945"],
|
||||||
|
["testuser"],
|
||||||
|
)
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def get_space_buyers(
|
||||||
|
credentials: TwitterCredentials,
|
||||||
|
space_id: str,
|
||||||
|
expansions: UserExpansionsFilter | None,
|
||||||
|
user_fields: TweetUserFieldsFilter | None,
|
||||||
|
):
|
||||||
|
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": None,
|
||||||
|
"media_fields": None,
|
||||||
|
"place_fields": None,
|
||||||
|
"poll_fields": None,
|
||||||
|
"tweet_fields": None,
|
||||||
|
"user_fields": None,
|
||||||
|
},
|
||||||
|
test_credentials=TEST_CREDENTIALS,
|
||||||
|
test_output=[
|
||||||
|
("tweet_ids", ["1234567890"]),
|
||||||
|
("texts", ["Test tweet"]),
|
||||||
|
("data", [{"id": "1234567890", "text": "Test tweet"}]),
|
||||||
|
],
|
||||||
|
test_mock={
|
||||||
|
"get_space_tweets": lambda *args, **kwargs: (
|
||||||
|
[{"id": "1234567890", "text": "Test tweet"}], # data
|
||||||
|
{},
|
||||||
|
["1234567890"],
|
||||||
|
["Test tweet"],
|
||||||
|
{},
|
||||||
|
)
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def get_space_tweets(
|
||||||
|
credentials: TwitterCredentials,
|
||||||
|
space_id: str,
|
||||||
|
expansions: ExpansionFilter | None,
|
||||||
|
media_fields: TweetMediaFieldsFilter | None,
|
||||||
|
place_fields: TweetPlaceFieldsFilter | None,
|
||||||
|
poll_fields: TweetPollFieldsFilter | None,
|
||||||
|
tweet_fields: TweetFieldsFilter | None,
|
||||||
|
user_fields: TweetUserFieldsFilter | None,
|
||||||
|
):
|
||||||
|
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)
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
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)}"
|
||||||
@@ -0,0 +1,372 @@
|
|||||||
|
from typing import cast
|
||||||
|
|
||||||
|
import tweepy
|
||||||
|
from tweepy.client import Response
|
||||||
|
|
||||||
|
from backend.blocks.twitter._auth import (
|
||||||
|
TEST_CREDENTIALS,
|
||||||
|
TEST_CREDENTIALS_INPUT,
|
||||||
|
TwitterCredentials,
|
||||||
|
TwitterCredentialsField,
|
||||||
|
TwitterCredentialsInput,
|
||||||
|
)
|
||||||
|
from backend.blocks.twitter._builders import TweetExpansionsBuilder
|
||||||
|
from backend.blocks.twitter._serializer import (
|
||||||
|
IncludesSerializer,
|
||||||
|
ResponseDataSerializer,
|
||||||
|
)
|
||||||
|
from backend.blocks.twitter._types import (
|
||||||
|
ExpansionFilter,
|
||||||
|
TweetExpansionInputs,
|
||||||
|
TweetFieldsFilter,
|
||||||
|
TweetMediaFieldsFilter,
|
||||||
|
TweetPlaceFieldsFilter,
|
||||||
|
TweetPollFieldsFilter,
|
||||||
|
TweetUserFieldsFilter,
|
||||||
|
)
|
||||||
|
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
|
||||||
|
|
||||||
|
|
||||||
|
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),
|
||||||
|
],
|
||||||
|
test_mock={"bookmark_tweet": lambda *args, **kwargs: 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 | None = 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": 2,
|
||||||
|
"pagination_token": None,
|
||||||
|
"expansions": None,
|
||||||
|
"media_fields": None,
|
||||||
|
"place_fields": None,
|
||||||
|
"poll_fields": None,
|
||||||
|
"tweet_fields": None,
|
||||||
|
"user_fields": None,
|
||||||
|
"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"}]),
|
||||||
|
],
|
||||||
|
test_mock={
|
||||||
|
"get_bookmarked_tweets": lambda *args, **kwargs: (
|
||||||
|
["1234567890"],
|
||||||
|
["Test tweet"],
|
||||||
|
["12345"],
|
||||||
|
["testuser"],
|
||||||
|
[{"id": "1234567890", "text": "Test tweet"}],
|
||||||
|
{},
|
||||||
|
{},
|
||||||
|
None,
|
||||||
|
)
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def get_bookmarked_tweets(
|
||||||
|
credentials: TwitterCredentials,
|
||||||
|
max_results: int,
|
||||||
|
pagination_token: str | None,
|
||||||
|
expansions: ExpansionFilter | None,
|
||||||
|
media_fields: TweetMediaFieldsFilter | None,
|
||||||
|
place_fields: TweetPlaceFieldsFilter | None,
|
||||||
|
poll_fields: TweetPollFieldsFilter | None,
|
||||||
|
tweet_fields: TweetFieldsFilter | None,
|
||||||
|
user_fields: TweetUserFieldsFilter | None,
|
||||||
|
):
|
||||||
|
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)
|
||||||
154
autogpt_platform/backend/backend/blocks/twitter/tweets/hide.py
Normal file
154
autogpt_platform/backend/backend/blocks/twitter/tweets/hide.py
Normal file
@@ -0,0 +1,154 @@
|
|||||||
|
import tweepy
|
||||||
|
|
||||||
|
from backend.blocks.twitter._auth import (
|
||||||
|
TEST_CREDENTIALS,
|
||||||
|
TEST_CREDENTIALS_INPUT,
|
||||||
|
TwitterCredentials,
|
||||||
|
TwitterCredentialsField,
|
||||||
|
TwitterCredentialsInput,
|
||||||
|
)
|
||||||
|
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
|
||||||
|
|
||||||
|
|
||||||
|
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),
|
||||||
|
],
|
||||||
|
test_mock={"hide_reply": lambda *args, **kwargs: 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),
|
||||||
|
],
|
||||||
|
test_mock={"unhide_reply": lambda *args, **kwargs: 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)
|
||||||
576
autogpt_platform/backend/backend/blocks/twitter/tweets/like.py
Normal file
576
autogpt_platform/backend/backend/blocks/twitter/tweets/like.py
Normal file
@@ -0,0 +1,576 @@
|
|||||||
|
from typing import cast
|
||||||
|
|
||||||
|
import tweepy
|
||||||
|
from tweepy.client import Response
|
||||||
|
|
||||||
|
from backend.blocks.twitter._auth import (
|
||||||
|
TEST_CREDENTIALS,
|
||||||
|
TEST_CREDENTIALS_INPUT,
|
||||||
|
TwitterCredentials,
|
||||||
|
TwitterCredentialsField,
|
||||||
|
TwitterCredentialsInput,
|
||||||
|
)
|
||||||
|
from backend.blocks.twitter._builders import (
|
||||||
|
TweetExpansionsBuilder,
|
||||||
|
UserExpansionsBuilder,
|
||||||
|
)
|
||||||
|
from backend.blocks.twitter._serializer import (
|
||||||
|
IncludesSerializer,
|
||||||
|
ResponseDataSerializer,
|
||||||
|
)
|
||||||
|
from backend.blocks.twitter._types import (
|
||||||
|
ExpansionFilter,
|
||||||
|
TweetExpansionInputs,
|
||||||
|
TweetFieldsFilter,
|
||||||
|
TweetMediaFieldsFilter,
|
||||||
|
TweetPlaceFieldsFilter,
|
||||||
|
TweetPollFieldsFilter,
|
||||||
|
TweetUserFieldsFilter,
|
||||||
|
UserExpansionInputs,
|
||||||
|
UserExpansionsFilter,
|
||||||
|
)
|
||||||
|
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
|
||||||
|
|
||||||
|
|
||||||
|
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),
|
||||||
|
],
|
||||||
|
test_mock={"like_tweet": lambda *args, **kwargs: 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 | None = 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": 1,
|
||||||
|
"pagination_token": None,
|
||||||
|
"credentials": TEST_CREDENTIALS_INPUT,
|
||||||
|
"expansions": None,
|
||||||
|
"tweet_fields": None,
|
||||||
|
"user_fields": None,
|
||||||
|
},
|
||||||
|
test_credentials=TEST_CREDENTIALS,
|
||||||
|
test_output=[
|
||||||
|
("id", ["1234567890"]),
|
||||||
|
("username", ["testuser"]),
|
||||||
|
("data", [{"id": "1234567890", "username": "testuser"}]),
|
||||||
|
],
|
||||||
|
test_mock={
|
||||||
|
"get_liking_users": lambda *args, **kwargs: (
|
||||||
|
["1234567890"],
|
||||||
|
["testuser"],
|
||||||
|
[{"id": "1234567890", "username": "testuser"}],
|
||||||
|
{},
|
||||||
|
{},
|
||||||
|
None,
|
||||||
|
)
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def get_liking_users(
|
||||||
|
credentials: TwitterCredentials,
|
||||||
|
tweet_id: str,
|
||||||
|
max_results: int,
|
||||||
|
pagination_token: str | None,
|
||||||
|
expansions: UserExpansionsFilter | None,
|
||||||
|
tweet_fields: TweetFieldsFilter | None,
|
||||||
|
user_fields: TweetUserFieldsFilter | None,
|
||||||
|
):
|
||||||
|
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 | None = 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": 2,
|
||||||
|
"pagination_token": None,
|
||||||
|
"credentials": TEST_CREDENTIALS_INPUT,
|
||||||
|
"expansions": None,
|
||||||
|
"media_fields": None,
|
||||||
|
"place_fields": None,
|
||||||
|
"poll_fields": None,
|
||||||
|
"tweet_fields": None,
|
||||||
|
"user_fields": None,
|
||||||
|
},
|
||||||
|
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"},
|
||||||
|
],
|
||||||
|
),
|
||||||
|
],
|
||||||
|
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"},
|
||||||
|
],
|
||||||
|
{},
|
||||||
|
{},
|
||||||
|
None,
|
||||||
|
)
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def get_liked_tweets(
|
||||||
|
credentials: TwitterCredentials,
|
||||||
|
user_id: str,
|
||||||
|
max_results: int,
|
||||||
|
pagination_token: str | None,
|
||||||
|
expansions: ExpansionFilter | None,
|
||||||
|
media_fields: TweetMediaFieldsFilter | None,
|
||||||
|
place_fields: TweetPlaceFieldsFilter | None,
|
||||||
|
poll_fields: TweetPollFieldsFilter | None,
|
||||||
|
tweet_fields: TweetFieldsFilter | None,
|
||||||
|
user_fields: TweetUserFieldsFilter | None,
|
||||||
|
):
|
||||||
|
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),
|
||||||
|
],
|
||||||
|
test_mock={"unlike_tweet": lambda *args, **kwargs: 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)
|
||||||
557
autogpt_platform/backend/backend/blocks/twitter/tweets/manage.py
Normal file
557
autogpt_platform/backend/backend/blocks/twitter/tweets/manage.py
Normal file
@@ -0,0 +1,557 @@
|
|||||||
|
from datetime import datetime
|
||||||
|
from typing import cast
|
||||||
|
|
||||||
|
import tweepy
|
||||||
|
from tweepy.client import Response
|
||||||
|
|
||||||
|
from backend.blocks.twitter._auth import (
|
||||||
|
TEST_CREDENTIALS,
|
||||||
|
TEST_CREDENTIALS_INPUT,
|
||||||
|
TwitterCredentials,
|
||||||
|
TwitterCredentialsField,
|
||||||
|
TwitterCredentialsInput,
|
||||||
|
)
|
||||||
|
from backend.blocks.twitter._builders import (
|
||||||
|
TweetDurationBuilder,
|
||||||
|
TweetExpansionsBuilder,
|
||||||
|
TweetPostBuilder,
|
||||||
|
TweetSearchBuilder,
|
||||||
|
)
|
||||||
|
from backend.blocks.twitter._serializer import (
|
||||||
|
IncludesSerializer,
|
||||||
|
ResponseDataSerializer,
|
||||||
|
)
|
||||||
|
from backend.blocks.twitter._types import (
|
||||||
|
ExpansionFilter,
|
||||||
|
TweetExpansionInputs,
|
||||||
|
TweetFieldsFilter,
|
||||||
|
TweetMediaFieldsFilter,
|
||||||
|
TweetPlaceFieldsFilter,
|
||||||
|
TweetPollFieldsFilter,
|
||||||
|
TweetReplySettingsFilter,
|
||||||
|
TweetTimeWindowInputs,
|
||||||
|
TweetUserFieldsFilter,
|
||||||
|
)
|
||||||
|
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
|
||||||
|
|
||||||
|
|
||||||
|
class TwitterPostTweetBlock(Block):
|
||||||
|
"""
|
||||||
|
Create a tweet on Twitter with the option to include one additional element such as a media, quote, or deep link.
|
||||||
|
"""
|
||||||
|
|
||||||
|
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: TweetReplySettingsFilter = SchemaField(
|
||||||
|
description="Who can reply to the Tweet (mentionedUsers or following)",
|
||||||
|
placeholder="Enter reply settings",
|
||||||
|
advanced=True,
|
||||||
|
default=TweetReplySettingsFilter(All_Users=True),
|
||||||
|
)
|
||||||
|
|
||||||
|
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": "",
|
||||||
|
},
|
||||||
|
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: TweetReplySettingsFilter,
|
||||||
|
):
|
||||||
|
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:
|
||||||
|
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:
|
||||||
|
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 | None = 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": 2,
|
||||||
|
"start_time": "2024-12-14T18:30:00.000Z",
|
||||||
|
"end_time": "2024-12-17T18:30:00.000Z",
|
||||||
|
"since_id": None,
|
||||||
|
"until_id": None,
|
||||||
|
"sort_order": None,
|
||||||
|
"pagination": None,
|
||||||
|
"expansions": None,
|
||||||
|
"media_fields": None,
|
||||||
|
"place_fields": None,
|
||||||
|
"poll_fields": None,
|
||||||
|
"tweet_fields": None,
|
||||||
|
"user_fields": None,
|
||||||
|
},
|
||||||
|
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!",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
),
|
||||||
|
],
|
||||||
|
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!",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
{},
|
||||||
|
{},
|
||||||
|
None,
|
||||||
|
)
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def search_tweets(
|
||||||
|
credentials: TwitterCredentials,
|
||||||
|
query: str,
|
||||||
|
max_results: int,
|
||||||
|
start_time: datetime | None,
|
||||||
|
end_time: datetime | None,
|
||||||
|
since_id: str | None,
|
||||||
|
until_id: str | None,
|
||||||
|
sort_order: str | None,
|
||||||
|
pagination: str | None,
|
||||||
|
expansions: ExpansionFilter | None,
|
||||||
|
media_fields: TweetMediaFieldsFilter | None,
|
||||||
|
place_fields: TweetPlaceFieldsFilter | None,
|
||||||
|
poll_fields: TweetPollFieldsFilter | None,
|
||||||
|
tweet_fields: TweetFieldsFilter | None,
|
||||||
|
user_fields: TweetUserFieldsFilter | None,
|
||||||
|
):
|
||||||
|
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)
|
||||||
224
autogpt_platform/backend/backend/blocks/twitter/tweets/quote.py
Normal file
224
autogpt_platform/backend/backend/blocks/twitter/tweets/quote.py
Normal file
@@ -0,0 +1,224 @@
|
|||||||
|
from typing import cast
|
||||||
|
|
||||||
|
import tweepy
|
||||||
|
from tweepy.client import Response
|
||||||
|
|
||||||
|
from backend.blocks.twitter._auth import (
|
||||||
|
TEST_CREDENTIALS,
|
||||||
|
TEST_CREDENTIALS_INPUT,
|
||||||
|
TwitterCredentials,
|
||||||
|
TwitterCredentialsField,
|
||||||
|
TwitterCredentialsInput,
|
||||||
|
)
|
||||||
|
from backend.blocks.twitter._builders import TweetExpansionsBuilder
|
||||||
|
from backend.blocks.twitter._serializer import (
|
||||||
|
IncludesSerializer,
|
||||||
|
ResponseDataSerializer,
|
||||||
|
)
|
||||||
|
from backend.blocks.twitter._types import (
|
||||||
|
ExpansionFilter,
|
||||||
|
TweetExcludesFilter,
|
||||||
|
TweetExpansionInputs,
|
||||||
|
TweetFieldsFilter,
|
||||||
|
TweetMediaFieldsFilter,
|
||||||
|
TweetPlaceFieldsFilter,
|
||||||
|
TweetPollFieldsFilter,
|
||||||
|
TweetUserFieldsFilter,
|
||||||
|
)
|
||||||
|
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
|
||||||
|
|
||||||
|
|
||||||
|
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,
|
||||||
|
advanced=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
exclude: TweetExcludesFilter | None = SchemaField(
|
||||||
|
description="Types of tweets to exclude",
|
||||||
|
advanced=True,
|
||||||
|
default=None
|
||||||
|
)
|
||||||
|
|
||||||
|
pagination_token: str | None = SchemaField(
|
||||||
|
description="Token for pagination",
|
||||||
|
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": 2,
|
||||||
|
"pagination_token": None,
|
||||||
|
"credentials": TEST_CREDENTIALS_INPUT,
|
||||||
|
},
|
||||||
|
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"},
|
||||||
|
],
|
||||||
|
),
|
||||||
|
],
|
||||||
|
test_mock={
|
||||||
|
"get_quote_tweets": lambda *args, **kwargs: (
|
||||||
|
["12345", "67890"],
|
||||||
|
["Tweet 1", "Tweet 2"],
|
||||||
|
[
|
||||||
|
{"id": "12345", "text": "Tweet 1"},
|
||||||
|
{"id": "67890", "text": "Tweet 2"},
|
||||||
|
],
|
||||||
|
{},
|
||||||
|
{},
|
||||||
|
None,
|
||||||
|
)
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def get_quote_tweets(
|
||||||
|
credentials: TwitterCredentials,
|
||||||
|
tweet_id: str,
|
||||||
|
max_results: int,
|
||||||
|
exclude: TweetExcludesFilter | None,
|
||||||
|
pagination_token: str | None,
|
||||||
|
expansions: ExpansionFilter | None,
|
||||||
|
media_fields: TweetMediaFieldsFilter | None,
|
||||||
|
place_fields: TweetPlaceFieldsFilter | None,
|
||||||
|
poll_fields: TweetPollFieldsFilter | None,
|
||||||
|
tweet_fields: TweetFieldsFilter | None,
|
||||||
|
user_fields: TweetUserFieldsFilter | None,
|
||||||
|
):
|
||||||
|
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 == TweetExcludesFilter() 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)
|
||||||
@@ -0,0 +1,363 @@
|
|||||||
|
from typing import cast
|
||||||
|
|
||||||
|
import tweepy
|
||||||
|
from tweepy.client import Response
|
||||||
|
|
||||||
|
from backend.blocks.twitter._auth import (
|
||||||
|
TEST_CREDENTIALS,
|
||||||
|
TEST_CREDENTIALS_INPUT,
|
||||||
|
TwitterCredentials,
|
||||||
|
TwitterCredentialsField,
|
||||||
|
TwitterCredentialsInput,
|
||||||
|
)
|
||||||
|
from backend.blocks.twitter._builders import UserExpansionsBuilder
|
||||||
|
from backend.blocks.twitter._serializer import (
|
||||||
|
IncludesSerializer,
|
||||||
|
ResponseDataSerializer,
|
||||||
|
)
|
||||||
|
from backend.blocks.twitter._types import (
|
||||||
|
TweetFieldsFilter,
|
||||||
|
TweetUserFieldsFilter,
|
||||||
|
UserExpansionInputs,
|
||||||
|
UserExpansionsFilter,
|
||||||
|
)
|
||||||
|
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
|
||||||
|
|
||||||
|
|
||||||
|
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),
|
||||||
|
],
|
||||||
|
test_mock={"retweet": lambda *args, **kwargs: True},
|
||||||
|
)
|
||||||
|
|
||||||
|
@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),
|
||||||
|
],
|
||||||
|
test_mock={"remove_retweet": lambda *args, **kwargs: 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 | None = 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": None,
|
||||||
|
"media_fields": None,
|
||||||
|
"place_fields": None,
|
||||||
|
"poll_fields": None,
|
||||||
|
"tweet_fields": None,
|
||||||
|
"user_fields": None,
|
||||||
|
},
|
||||||
|
test_credentials=TEST_CREDENTIALS,
|
||||||
|
test_output=[
|
||||||
|
("ids", ["12345"]),
|
||||||
|
("names", ["Test User"]),
|
||||||
|
("usernames", ["testuser"]),
|
||||||
|
(
|
||||||
|
"data",
|
||||||
|
[{"id": "12345", "name": "Test User", "username": "testuser"}],
|
||||||
|
),
|
||||||
|
],
|
||||||
|
test_mock={
|
||||||
|
"get_retweeters": lambda *args, **kwargs: (
|
||||||
|
[{"id": "12345", "name": "Test User", "username": "testuser"}],
|
||||||
|
{},
|
||||||
|
{},
|
||||||
|
["12345"],
|
||||||
|
["Test User"],
|
||||||
|
["testuser"],
|
||||||
|
None,
|
||||||
|
)
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def get_retweeters(
|
||||||
|
credentials: TwitterCredentials,
|
||||||
|
tweet_id: str,
|
||||||
|
max_results: int,
|
||||||
|
pagination_token: str | None,
|
||||||
|
expansions: UserExpansionsFilter | None,
|
||||||
|
tweet_fields: TweetFieldsFilter | None,
|
||||||
|
user_fields: TweetUserFieldsFilter | None,
|
||||||
|
):
|
||||||
|
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)
|
||||||
@@ -0,0 +1,757 @@
|
|||||||
|
from datetime import datetime
|
||||||
|
from typing import cast
|
||||||
|
|
||||||
|
import tweepy
|
||||||
|
from tweepy.client import Response
|
||||||
|
|
||||||
|
from backend.blocks.twitter._auth import (
|
||||||
|
TEST_CREDENTIALS,
|
||||||
|
TEST_CREDENTIALS_INPUT,
|
||||||
|
TwitterCredentials,
|
||||||
|
TwitterCredentialsField,
|
||||||
|
TwitterCredentialsInput,
|
||||||
|
)
|
||||||
|
from backend.blocks.twitter._builders import (
|
||||||
|
TweetDurationBuilder,
|
||||||
|
TweetExpansionsBuilder,
|
||||||
|
)
|
||||||
|
from backend.blocks.twitter._serializer import (
|
||||||
|
IncludesSerializer,
|
||||||
|
ResponseDataSerializer,
|
||||||
|
)
|
||||||
|
from backend.blocks.twitter._types import (
|
||||||
|
ExpansionFilter,
|
||||||
|
TweetExpansionInputs,
|
||||||
|
TweetFieldsFilter,
|
||||||
|
TweetMediaFieldsFilter,
|
||||||
|
TweetPlaceFieldsFilter,
|
||||||
|
TweetPollFieldsFilter,
|
||||||
|
TweetTimeWindowInputs,
|
||||||
|
TweetUserFieldsFilter,
|
||||||
|
)
|
||||||
|
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
|
||||||
|
|
||||||
|
|
||||||
|
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 | None = 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": 2,
|
||||||
|
"start_time": "2024-12-14T18:30:00.000Z",
|
||||||
|
"end_time": "2024-12-17T18:30:00.000Z",
|
||||||
|
"since_id": "",
|
||||||
|
"until_id": "",
|
||||||
|
"sort_order": None,
|
||||||
|
"pagination_token": None,
|
||||||
|
"expansions": None,
|
||||||
|
"media_fields": None,
|
||||||
|
"place_fields": None,
|
||||||
|
"poll_fields": None,
|
||||||
|
"tweet_fields": None,
|
||||||
|
"user_fields": None,
|
||||||
|
},
|
||||||
|
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"},
|
||||||
|
],
|
||||||
|
),
|
||||||
|
],
|
||||||
|
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"},
|
||||||
|
],
|
||||||
|
{},
|
||||||
|
{},
|
||||||
|
None,
|
||||||
|
)
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def get_mentions(
|
||||||
|
credentials: TwitterCredentials,
|
||||||
|
user_id: str,
|
||||||
|
max_results: int,
|
||||||
|
start_time: datetime | None,
|
||||||
|
end_time: datetime | None,
|
||||||
|
since_id: str | None,
|
||||||
|
until_id: str | None,
|
||||||
|
sort_order: str | None,
|
||||||
|
pagination: str | None,
|
||||||
|
expansions: ExpansionFilter | None,
|
||||||
|
media_fields: TweetMediaFieldsFilter | None,
|
||||||
|
place_fields: TweetPlaceFieldsFilter | None,
|
||||||
|
poll_fields: TweetPollFieldsFilter | None,
|
||||||
|
tweet_fields: TweetFieldsFilter | None,
|
||||||
|
user_fields: TweetUserFieldsFilter | None,
|
||||||
|
):
|
||||||
|
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 | None = 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": 2,
|
||||||
|
"start_time": "2024-12-14T18:30:00.000Z",
|
||||||
|
"end_time": "2024-12-17T18:30:00.000Z",
|
||||||
|
"since_id": None,
|
||||||
|
"until_id": None,
|
||||||
|
"sort_order": None,
|
||||||
|
"pagination_token": None,
|
||||||
|
},
|
||||||
|
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"},
|
||||||
|
],
|
||||||
|
),
|
||||||
|
],
|
||||||
|
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"},
|
||||||
|
],
|
||||||
|
{},
|
||||||
|
{},
|
||||||
|
None,
|
||||||
|
)
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def get_timeline(
|
||||||
|
credentials: TwitterCredentials,
|
||||||
|
max_results: int,
|
||||||
|
start_time: datetime | None,
|
||||||
|
end_time: datetime | None,
|
||||||
|
since_id: str | None,
|
||||||
|
until_id: str | None,
|
||||||
|
sort_order: str | None,
|
||||||
|
pagination: str | None,
|
||||||
|
expansions: ExpansionFilter | None,
|
||||||
|
media_fields: TweetMediaFieldsFilter | None,
|
||||||
|
place_fields: TweetPlaceFieldsFilter | None,
|
||||||
|
poll_fields: TweetPollFieldsFilter | None,
|
||||||
|
tweet_fields: TweetFieldsFilter | None,
|
||||||
|
user_fields: TweetUserFieldsFilter | None,
|
||||||
|
):
|
||||||
|
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 | None = 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": 2,
|
||||||
|
"start_time": "2024-12-14T18:30:00.000Z",
|
||||||
|
"end_time": "2024-12-17T18:30:00.000Z",
|
||||||
|
"since_id": None,
|
||||||
|
"until_id": None,
|
||||||
|
"sort_order": None,
|
||||||
|
"pagination_token": None,
|
||||||
|
"expansions": None,
|
||||||
|
"media_fields": None,
|
||||||
|
"place_fields": None,
|
||||||
|
"poll_fields": None,
|
||||||
|
"tweet_fields": None,
|
||||||
|
"user_fields": None,
|
||||||
|
},
|
||||||
|
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"},
|
||||||
|
],
|
||||||
|
),
|
||||||
|
],
|
||||||
|
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"},
|
||||||
|
],
|
||||||
|
{},
|
||||||
|
{},
|
||||||
|
None,
|
||||||
|
)
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def get_user_tweets(
|
||||||
|
credentials: TwitterCredentials,
|
||||||
|
user_id: str,
|
||||||
|
max_results: int,
|
||||||
|
start_time: datetime | None,
|
||||||
|
end_time: datetime | None,
|
||||||
|
since_id: str | None,
|
||||||
|
until_id: str | None,
|
||||||
|
sort_order: str | None,
|
||||||
|
pagination: str | None,
|
||||||
|
expansions: ExpansionFilter | None,
|
||||||
|
media_fields: TweetMediaFieldsFilter | None,
|
||||||
|
place_fields: TweetPlaceFieldsFilter | None,
|
||||||
|
poll_fields: TweetPollFieldsFilter | None,
|
||||||
|
tweet_fields: TweetFieldsFilter | None,
|
||||||
|
user_fields: TweetUserFieldsFilter | None,
|
||||||
|
):
|
||||||
|
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)
|
||||||
@@ -0,0 +1,361 @@
|
|||||||
|
from typing import cast
|
||||||
|
|
||||||
|
import tweepy
|
||||||
|
from tweepy.client import Response
|
||||||
|
|
||||||
|
from backend.blocks.twitter._auth import (
|
||||||
|
TEST_CREDENTIALS,
|
||||||
|
TEST_CREDENTIALS_INPUT,
|
||||||
|
TwitterCredentials,
|
||||||
|
TwitterCredentialsField,
|
||||||
|
TwitterCredentialsInput,
|
||||||
|
)
|
||||||
|
from backend.blocks.twitter._builders import TweetExpansionsBuilder
|
||||||
|
from backend.blocks.twitter._serializer import (
|
||||||
|
IncludesSerializer,
|
||||||
|
ResponseDataSerializer,
|
||||||
|
)
|
||||||
|
from backend.blocks.twitter._types import (
|
||||||
|
ExpansionFilter,
|
||||||
|
TweetExpansionInputs,
|
||||||
|
TweetFieldsFilter,
|
||||||
|
TweetMediaFieldsFilter,
|
||||||
|
TweetPlaceFieldsFilter,
|
||||||
|
TweetPollFieldsFilter,
|
||||||
|
TweetUserFieldsFilter,
|
||||||
|
)
|
||||||
|
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
|
||||||
|
|
||||||
|
|
||||||
|
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": None,
|
||||||
|
"media_fields": None,
|
||||||
|
"place_fields": None,
|
||||||
|
"poll_fields": None,
|
||||||
|
"tweet_fields": None,
|
||||||
|
"user_fields": None,
|
||||||
|
},
|
||||||
|
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"},
|
||||||
|
{"users": [{"id": "12345", "username": "testuser"}]},
|
||||||
|
{"result_count": 1},
|
||||||
|
"12345",
|
||||||
|
"testuser",
|
||||||
|
)
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def get_tweet(
|
||||||
|
credentials: TwitterCredentials,
|
||||||
|
tweet_id: str,
|
||||||
|
expansions: ExpansionFilter | None,
|
||||||
|
media_fields: TweetMediaFieldsFilter | None,
|
||||||
|
place_fields: TweetPlaceFieldsFilter | None,
|
||||||
|
poll_fields: TweetPollFieldsFilter | None,
|
||||||
|
tweet_fields: TweetFieldsFilter | None,
|
||||||
|
user_fields: TweetUserFieldsFilter | None,
|
||||||
|
):
|
||||||
|
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": None,
|
||||||
|
"media_fields": None,
|
||||||
|
"place_fields": None,
|
||||||
|
"poll_fields": None,
|
||||||
|
"tweet_fields": None,
|
||||||
|
"user_fields": None,
|
||||||
|
},
|
||||||
|
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: (
|
||||||
|
["1460323737035677698"], # ids
|
||||||
|
["Test tweet content"], # texts
|
||||||
|
["67890"], # user_ids
|
||||||
|
["testuser1"], # user_names
|
||||||
|
[
|
||||||
|
{"id": "1460323737035677698", "text": "Test tweet content"}
|
||||||
|
], # data
|
||||||
|
{"users": [{"id": "67890", "username": "testuser1"}]}, # included
|
||||||
|
{"result_count": 1}, # meta
|
||||||
|
)
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def get_tweets(
|
||||||
|
credentials: TwitterCredentials,
|
||||||
|
tweet_ids: list[str],
|
||||||
|
expansions: ExpansionFilter | None,
|
||||||
|
media_fields: TweetMediaFieldsFilter | None,
|
||||||
|
place_fields: TweetPlaceFieldsFilter | None,
|
||||||
|
poll_fields: TweetPollFieldsFilter | None,
|
||||||
|
tweet_fields: TweetFieldsFilter | None,
|
||||||
|
user_fields: TweetUserFieldsFilter | None,
|
||||||
|
):
|
||||||
|
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)
|
||||||
305
autogpt_platform/backend/backend/blocks/twitter/users/blocks.py
Normal file
305
autogpt_platform/backend/backend/blocks/twitter/users/blocks.py
Normal file
@@ -0,0 +1,305 @@
|
|||||||
|
from typing import cast
|
||||||
|
|
||||||
|
import tweepy
|
||||||
|
from tweepy.client import Response
|
||||||
|
|
||||||
|
from backend.blocks.twitter._auth import (
|
||||||
|
TEST_CREDENTIALS,
|
||||||
|
TEST_CREDENTIALS_INPUT,
|
||||||
|
TwitterCredentials,
|
||||||
|
TwitterCredentialsField,
|
||||||
|
TwitterCredentialsInput,
|
||||||
|
)
|
||||||
|
from backend.blocks.twitter._builders import UserExpansionsBuilder
|
||||||
|
from backend.blocks.twitter._serializer import IncludesSerializer
|
||||||
|
from backend.blocks.twitter._types import (
|
||||||
|
TweetFieldsFilter,
|
||||||
|
TweetUserFieldsFilter,
|
||||||
|
UserExpansionInputs,
|
||||||
|
UserExpansionsFilter,
|
||||||
|
)
|
||||||
|
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
|
||||||
|
|
||||||
|
|
||||||
|
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),
|
||||||
|
],
|
||||||
|
test_mock={"unblock_user": lambda *args, **kwargs: 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 | None = 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": None,
|
||||||
|
"tweet_fields": None,
|
||||||
|
"user_fields": None,
|
||||||
|
},
|
||||||
|
test_credentials=TEST_CREDENTIALS,
|
||||||
|
test_output=[
|
||||||
|
("user_ids", ["12345", "67890"]),
|
||||||
|
("usernames_", ["testuser1", "testuser2"]),
|
||||||
|
],
|
||||||
|
test_mock={
|
||||||
|
"get_blocked_users": lambda *args, **kwargs: (
|
||||||
|
{}, # included
|
||||||
|
{}, # meta
|
||||||
|
["12345", "67890"], # user_ids
|
||||||
|
["testuser1", "testuser2"], # usernames
|
||||||
|
None, # next_token
|
||||||
|
)
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def get_blocked_users(
|
||||||
|
credentials: TwitterCredentials,
|
||||||
|
max_results: int,
|
||||||
|
pagination_token: str | None,
|
||||||
|
expansions: UserExpansionsFilter | None,
|
||||||
|
tweet_fields: TweetFieldsFilter | None,
|
||||||
|
user_fields: TweetUserFieldsFilter | None,
|
||||||
|
):
|
||||||
|
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),
|
||||||
|
],
|
||||||
|
test_mock={"block_user": lambda *args, **kwargs: 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)
|
||||||
506
autogpt_platform/backend/backend/blocks/twitter/users/follows.py
Normal file
506
autogpt_platform/backend/backend/blocks/twitter/users/follows.py
Normal file
@@ -0,0 +1,506 @@
|
|||||||
|
from typing import cast
|
||||||
|
|
||||||
|
import tweepy
|
||||||
|
from tweepy.client import Response
|
||||||
|
|
||||||
|
from backend.blocks.twitter._auth import (
|
||||||
|
TEST_CREDENTIALS,
|
||||||
|
TEST_CREDENTIALS_INPUT,
|
||||||
|
TwitterCredentials,
|
||||||
|
TwitterCredentialsField,
|
||||||
|
TwitterCredentialsInput,
|
||||||
|
)
|
||||||
|
from backend.blocks.twitter._builders import UserExpansionsBuilder
|
||||||
|
from backend.blocks.twitter._serializer import (
|
||||||
|
IncludesSerializer,
|
||||||
|
ResponseDataSerializer,
|
||||||
|
)
|
||||||
|
from backend.blocks.twitter._types import (
|
||||||
|
TweetFieldsFilter,
|
||||||
|
TweetUserFieldsFilter,
|
||||||
|
UserExpansionInputs,
|
||||||
|
UserExpansionsFilter,
|
||||||
|
)
|
||||||
|
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
|
||||||
|
|
||||||
|
|
||||||
|
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),
|
||||||
|
],
|
||||||
|
test_mock={"unfollow_user": lambda *args, **kwargs: 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)],
|
||||||
|
test_mock={"follow_user": lambda *args, **kwargs: True},
|
||||||
|
)
|
||||||
|
|
||||||
|
@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 | None = 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": 1,
|
||||||
|
"pagination_token": "",
|
||||||
|
"expansions": None,
|
||||||
|
"tweet_fields": None,
|
||||||
|
"user_fields": None,
|
||||||
|
"credentials": TEST_CREDENTIALS_INPUT,
|
||||||
|
},
|
||||||
|
test_credentials=TEST_CREDENTIALS,
|
||||||
|
test_output=[
|
||||||
|
("ids", ["1234567890"]),
|
||||||
|
("usernames", ["testuser"]),
|
||||||
|
("data", [{"id": "1234567890", "username": "testuser"}]),
|
||||||
|
],
|
||||||
|
test_mock={
|
||||||
|
"get_followers": lambda *args, **kwargs: (
|
||||||
|
["1234567890"],
|
||||||
|
["testuser"],
|
||||||
|
[{"id": "1234567890", "username": "testuser"}],
|
||||||
|
{},
|
||||||
|
{},
|
||||||
|
None,
|
||||||
|
)
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def get_followers(
|
||||||
|
credentials: TwitterCredentials,
|
||||||
|
target_user_id: str,
|
||||||
|
max_results: int,
|
||||||
|
pagination_token: str | None,
|
||||||
|
expansions: UserExpansionsFilter | None,
|
||||||
|
tweet_fields: TweetFieldsFilter | None,
|
||||||
|
user_fields: TweetUserFieldsFilter | None,
|
||||||
|
):
|
||||||
|
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 | None = 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": 1,
|
||||||
|
"pagination_token": None,
|
||||||
|
"expansions": None,
|
||||||
|
"tweet_fields": None,
|
||||||
|
"user_fields": None,
|
||||||
|
"credentials": TEST_CREDENTIALS_INPUT,
|
||||||
|
},
|
||||||
|
test_credentials=TEST_CREDENTIALS,
|
||||||
|
test_output=[
|
||||||
|
("ids", ["1234567890"]),
|
||||||
|
("usernames", ["testuser"]),
|
||||||
|
("data", [{"id": "1234567890", "username": "testuser"}]),
|
||||||
|
],
|
||||||
|
test_mock={
|
||||||
|
"get_following": lambda *args, **kwargs: (
|
||||||
|
["1234567890"],
|
||||||
|
["testuser"],
|
||||||
|
[{"id": "1234567890", "username": "testuser"}],
|
||||||
|
{},
|
||||||
|
{},
|
||||||
|
None,
|
||||||
|
)
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def get_following(
|
||||||
|
credentials: TwitterCredentials,
|
||||||
|
target_user_id: str,
|
||||||
|
max_results: int,
|
||||||
|
pagination_token: str | None,
|
||||||
|
expansions: UserExpansionsFilter | None,
|
||||||
|
tweet_fields: TweetFieldsFilter | None,
|
||||||
|
user_fields: TweetUserFieldsFilter | None,
|
||||||
|
):
|
||||||
|
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)
|
||||||
327
autogpt_platform/backend/backend/blocks/twitter/users/mutes.py
Normal file
327
autogpt_platform/backend/backend/blocks/twitter/users/mutes.py
Normal file
@@ -0,0 +1,327 @@
|
|||||||
|
from typing import cast
|
||||||
|
|
||||||
|
import tweepy
|
||||||
|
from tweepy.client import Response
|
||||||
|
|
||||||
|
from backend.blocks.twitter._auth import (
|
||||||
|
TEST_CREDENTIALS,
|
||||||
|
TEST_CREDENTIALS_INPUT,
|
||||||
|
TwitterCredentials,
|
||||||
|
TwitterCredentialsField,
|
||||||
|
TwitterCredentialsInput,
|
||||||
|
)
|
||||||
|
from backend.blocks.twitter._builders import UserExpansionsBuilder
|
||||||
|
from backend.blocks.twitter._serializer import (
|
||||||
|
IncludesSerializer,
|
||||||
|
ResponseDataSerializer,
|
||||||
|
)
|
||||||
|
from backend.blocks.twitter._types import (
|
||||||
|
TweetFieldsFilter,
|
||||||
|
TweetUserFieldsFilter,
|
||||||
|
UserExpansionInputs,
|
||||||
|
UserExpansionsFilter,
|
||||||
|
)
|
||||||
|
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
|
||||||
|
|
||||||
|
|
||||||
|
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),
|
||||||
|
],
|
||||||
|
test_mock={"unmute_user": lambda *args, **kwargs: 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 | None = 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": 2,
|
||||||
|
"pagination_token": "",
|
||||||
|
"credentials": TEST_CREDENTIALS_INPUT,
|
||||||
|
"expansions": None,
|
||||||
|
"tweet_fields": None,
|
||||||
|
"user_fields": None,
|
||||||
|
},
|
||||||
|
test_credentials=TEST_CREDENTIALS,
|
||||||
|
test_output=[
|
||||||
|
("ids", ["12345", "67890"]),
|
||||||
|
("usernames", ["testuser1", "testuser2"]),
|
||||||
|
(
|
||||||
|
"data",
|
||||||
|
[
|
||||||
|
{"id": "12345", "username": "testuser1"},
|
||||||
|
{"id": "67890", "username": "testuser2"},
|
||||||
|
],
|
||||||
|
),
|
||||||
|
],
|
||||||
|
test_mock={
|
||||||
|
"get_muted_users": lambda *args, **kwargs: (
|
||||||
|
["12345", "67890"],
|
||||||
|
["testuser1", "testuser2"],
|
||||||
|
[
|
||||||
|
{"id": "12345", "username": "testuser1"},
|
||||||
|
{"id": "67890", "username": "testuser2"},
|
||||||
|
],
|
||||||
|
{},
|
||||||
|
{},
|
||||||
|
None,
|
||||||
|
)
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def get_muted_users(
|
||||||
|
credentials: TwitterCredentials,
|
||||||
|
max_results: int,
|
||||||
|
pagination_token: str | None,
|
||||||
|
expansions: UserExpansionsFilter | None,
|
||||||
|
tweet_fields: TweetFieldsFilter | None,
|
||||||
|
user_fields: TweetUserFieldsFilter | None,
|
||||||
|
):
|
||||||
|
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),
|
||||||
|
],
|
||||||
|
test_mock={"mute_user": lambda *args, **kwargs: 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)
|
||||||
@@ -0,0 +1,359 @@
|
|||||||
|
from typing import cast
|
||||||
|
|
||||||
|
import tweepy
|
||||||
|
from tweepy.client import Response
|
||||||
|
|
||||||
|
from backend.blocks.twitter._auth import (
|
||||||
|
TEST_CREDENTIALS,
|
||||||
|
TEST_CREDENTIALS_INPUT,
|
||||||
|
TwitterCredentials,
|
||||||
|
TwitterCredentialsField,
|
||||||
|
TwitterCredentialsInput,
|
||||||
|
)
|
||||||
|
from backend.blocks.twitter._builders import UserExpansionsBuilder
|
||||||
|
from backend.blocks.twitter._serializer import (
|
||||||
|
IncludesSerializer,
|
||||||
|
ResponseDataSerializer,
|
||||||
|
)
|
||||||
|
from backend.blocks.twitter._types import (
|
||||||
|
TweetFieldsFilter,
|
||||||
|
TweetUserFieldsFilter,
|
||||||
|
UserExpansionInputs,
|
||||||
|
UserExpansionsFilter,
|
||||||
|
)
|
||||||
|
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
|
||||||
|
|
||||||
|
|
||||||
|
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": "",
|
||||||
|
"username": "twitter",
|
||||||
|
"credentials": TEST_CREDENTIALS_INPUT,
|
||||||
|
"expansions": None,
|
||||||
|
"tweet_fields": None,
|
||||||
|
"user_fields": None,
|
||||||
|
},
|
||||||
|
test_credentials=TEST_CREDENTIALS,
|
||||||
|
test_output=[
|
||||||
|
("id", "783214"),
|
||||||
|
("username_", "twitter"),
|
||||||
|
("name_", "Twitter"),
|
||||||
|
(
|
||||||
|
"data",
|
||||||
|
{
|
||||||
|
"user": {
|
||||||
|
"id": "783214",
|
||||||
|
"username": "twitter",
|
||||||
|
"name": "Twitter",
|
||||||
|
}
|
||||||
|
},
|
||||||
|
),
|
||||||
|
],
|
||||||
|
test_mock={
|
||||||
|
"get_user": lambda *args, **kwargs: (
|
||||||
|
{
|
||||||
|
"user": {
|
||||||
|
"id": "783214",
|
||||||
|
"username": "twitter",
|
||||||
|
"name": "Twitter",
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{},
|
||||||
|
"twitter",
|
||||||
|
"783214",
|
||||||
|
"Twitter",
|
||||||
|
)
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def get_user(
|
||||||
|
credentials: TwitterCredentials,
|
||||||
|
user_id: str,
|
||||||
|
username: str,
|
||||||
|
expansions: UserExpansionsFilter | None,
|
||||||
|
tweet_fields: TweetFieldsFilter | None,
|
||||||
|
user_fields: TweetUserFieldsFilter | None,
|
||||||
|
):
|
||||||
|
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()
|
||||||
|
)
|
||||||
|
|
||||||
|
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": None,
|
||||||
|
"tweet_fields": None,
|
||||||
|
"user_fields": None,
|
||||||
|
},
|
||||||
|
test_credentials=TEST_CREDENTIALS,
|
||||||
|
test_output=[
|
||||||
|
("ids", ["783214", "2244994945"]),
|
||||||
|
("usernames_", ["twitter", "twitterdev"]),
|
||||||
|
("names_", ["Twitter", "Twitter Dev"]),
|
||||||
|
(
|
||||||
|
"data",
|
||||||
|
[
|
||||||
|
{"id": "783214", "username": "twitter", "name": "Twitter"},
|
||||||
|
{
|
||||||
|
"id": "2244994945",
|
||||||
|
"username": "twitterdev",
|
||||||
|
"name": "Twitter Dev",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
),
|
||||||
|
],
|
||||||
|
test_mock={
|
||||||
|
"get_users": lambda *args, **kwargs: (
|
||||||
|
[
|
||||||
|
{"id": "783214", "username": "twitter", "name": "Twitter"},
|
||||||
|
{
|
||||||
|
"id": "2244994945",
|
||||||
|
"username": "twitterdev",
|
||||||
|
"name": "Twitter Dev",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
{},
|
||||||
|
["twitter", "twitterdev"],
|
||||||
|
["783214", "2244994945"],
|
||||||
|
["Twitter", "Twitter Dev"],
|
||||||
|
)
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def get_users(
|
||||||
|
credentials: TwitterCredentials,
|
||||||
|
user_ids: list[str],
|
||||||
|
usernames: list[str],
|
||||||
|
expansions: UserExpansionsFilter | None,
|
||||||
|
tweet_fields: TweetFieldsFilter | None,
|
||||||
|
user_fields: TweetUserFieldsFilter | None,
|
||||||
|
):
|
||||||
|
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)
|
||||||
@@ -217,6 +217,7 @@ class OAuthState(BaseModel):
|
|||||||
token: str
|
token: str
|
||||||
provider: str
|
provider: str
|
||||||
expires_at: int
|
expires_at: int
|
||||||
|
code_verifier: Optional[str] = None
|
||||||
"""Unix timestamp (seconds) indicating when this OAuth state expires"""
|
"""Unix timestamp (seconds) indicating when this OAuth state expires"""
|
||||||
scopes: list[str]
|
scopes: list[str]
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
|
import base64
|
||||||
|
import hashlib
|
||||||
import secrets
|
import secrets
|
||||||
from datetime import datetime, timedelta, timezone
|
from datetime import datetime, timedelta, timezone
|
||||||
from typing import TYPE_CHECKING
|
from typing import TYPE_CHECKING, Optional
|
||||||
|
|
||||||
from pydantic import SecretStr
|
from pydantic import SecretStr
|
||||||
|
|
||||||
@@ -210,18 +212,24 @@ class IntegrationCredentialsStore:
|
|||||||
]
|
]
|
||||||
self._set_user_integration_creds(user_id, filtered_credentials)
|
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], use_pkce: bool = False
|
||||||
|
) -> tuple[str, str]:
|
||||||
token = secrets.token_urlsafe(32)
|
token = secrets.token_urlsafe(32)
|
||||||
expires_at = datetime.now(timezone.utc) + timedelta(minutes=10)
|
expires_at = datetime.now(timezone.utc) + timedelta(minutes=10)
|
||||||
|
|
||||||
|
(code_challenge, code_verifier) = self._generate_code_challenge()
|
||||||
|
|
||||||
state = OAuthState(
|
state = OAuthState(
|
||||||
token=token,
|
token=token,
|
||||||
provider=provider,
|
provider=provider,
|
||||||
|
code_verifier=code_verifier,
|
||||||
expires_at=int(expires_at.timestamp()),
|
expires_at=int(expires_at.timestamp()),
|
||||||
scopes=scopes,
|
scopes=scopes,
|
||||||
)
|
)
|
||||||
|
|
||||||
with self.locked_user_integrations(user_id):
|
with self.locked_user_integrations(user_id):
|
||||||
|
|
||||||
user_integrations = self._get_user_integrations(user_id)
|
user_integrations = self._get_user_integrations(user_id)
|
||||||
oauth_states = user_integrations.oauth_states
|
oauth_states = user_integrations.oauth_states
|
||||||
oauth_states.append(state)
|
oauth_states.append(state)
|
||||||
@@ -231,39 +239,21 @@ class IntegrationCredentialsStore:
|
|||||||
user_id=user_id, data=user_integrations
|
user_id=user_id, data=user_integrations
|
||||||
)
|
)
|
||||||
|
|
||||||
return token
|
return token, code_challenge
|
||||||
|
|
||||||
def get_any_valid_scopes_from_state_token(
|
def _generate_code_challenge(self) -> tuple[str, str]:
|
||||||
|
"""
|
||||||
|
Generate code challenge using SHA256 from the code verifier.
|
||||||
|
Currently only SHA256 is supported.(In future if we want to support more methods we can add them here)
|
||||||
|
"""
|
||||||
|
code_verifier = secrets.token_urlsafe(128)
|
||||||
|
sha256_hash = hashlib.sha256(code_verifier.encode("utf-8")).digest()
|
||||||
|
code_challenge = base64.urlsafe_b64encode(sha256_hash).decode("utf-8")
|
||||||
|
return code_challenge.replace("=", ""), code_verifier
|
||||||
|
|
||||||
|
def verify_state_token(
|
||||||
self, user_id: str, token: str, provider: str
|
self, user_id: str, token: str, provider: str
|
||||||
) -> list[str]:
|
) -> Optional[OAuthState]:
|
||||||
"""
|
|
||||||
Get the valid scopes from the OAuth state token. This will return any valid scopes
|
|
||||||
from any OAuth state token for the given provider. If no valid scopes are found,
|
|
||||||
an empty list is returned. DO NOT RELY ON THIS TOKEN TO AUTHENTICATE A USER, AS IT
|
|
||||||
IS TO CHECK IF THE USER HAS GIVEN PERMISSIONS TO THE APPLICATION BEFORE EXCHANGING
|
|
||||||
THE CODE FOR TOKENS.
|
|
||||||
"""
|
|
||||||
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.scopes
|
|
||||||
|
|
||||||
return []
|
|
||||||
|
|
||||||
def verify_state_token(self, user_id: str, token: str, provider: str) -> bool:
|
|
||||||
with self.locked_user_integrations(user_id):
|
with self.locked_user_integrations(user_id):
|
||||||
user_integrations = self._get_user_integrations(user_id)
|
user_integrations = self._get_user_integrations(user_id)
|
||||||
oauth_states = user_integrations.oauth_states
|
oauth_states = user_integrations.oauth_states
|
||||||
@@ -285,9 +275,9 @@ class IntegrationCredentialsStore:
|
|||||||
oauth_states.remove(valid_state)
|
oauth_states.remove(valid_state)
|
||||||
user_integrations.oauth_states = oauth_states
|
user_integrations.oauth_states = oauth_states
|
||||||
self.db_manager.update_user_integrations(user_id, user_integrations)
|
self.db_manager.update_user_integrations(user_id, user_integrations)
|
||||||
return True
|
return valid_state
|
||||||
|
|
||||||
return False
|
return None
|
||||||
|
|
||||||
def _set_user_integration_creds(
|
def _set_user_integration_creds(
|
||||||
self, user_id: str, credentials: list[Credentials]
|
self, user_id: str, credentials: list[Credentials]
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ from typing import TYPE_CHECKING
|
|||||||
from .github import GitHubOAuthHandler
|
from .github import GitHubOAuthHandler
|
||||||
from .google import GoogleOAuthHandler
|
from .google import GoogleOAuthHandler
|
||||||
from .notion import NotionOAuthHandler
|
from .notion import NotionOAuthHandler
|
||||||
|
from .twitter import TwitterOAuthHandler
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from ..providers import ProviderName
|
from ..providers import ProviderName
|
||||||
@@ -15,6 +16,7 @@ HANDLERS_BY_NAME: dict["ProviderName", type["BaseOAuthHandler"]] = {
|
|||||||
GitHubOAuthHandler,
|
GitHubOAuthHandler,
|
||||||
GoogleOAuthHandler,
|
GoogleOAuthHandler,
|
||||||
NotionOAuthHandler,
|
NotionOAuthHandler,
|
||||||
|
TwitterOAuthHandler,
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
# --8<-- [end:HANDLERS_BY_NAMEExample]
|
# --8<-- [end:HANDLERS_BY_NAMEExample]
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import logging
|
import logging
|
||||||
import time
|
import time
|
||||||
from abc import ABC, abstractmethod
|
from abc import ABC, abstractmethod
|
||||||
from typing import ClassVar
|
from typing import ClassVar, Optional
|
||||||
|
|
||||||
from backend.data.model import OAuth2Credentials
|
from backend.data.model import OAuth2Credentials
|
||||||
from backend.integrations.providers import ProviderName
|
from backend.integrations.providers import ProviderName
|
||||||
@@ -23,7 +23,9 @@ class BaseOAuthHandler(ABC):
|
|||||||
|
|
||||||
@abstractmethod
|
@abstractmethod
|
||||||
# --8<-- [start:BaseOAuthHandler3]
|
# --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]
|
# --8<-- [end:BaseOAuthHandler3]
|
||||||
"""Constructs a login URL that the user can be redirected to"""
|
"""Constructs a login URL that the user can be redirected to"""
|
||||||
...
|
...
|
||||||
@@ -31,7 +33,7 @@ class BaseOAuthHandler(ABC):
|
|||||||
@abstractmethod
|
@abstractmethod
|
||||||
# --8<-- [start:BaseOAuthHandler4]
|
# --8<-- [start:BaseOAuthHandler4]
|
||||||
def exchange_code_for_tokens(
|
def exchange_code_for_tokens(
|
||||||
self, code: str, scopes: list[str]
|
self, code: str, scopes: list[str], code_verifier: Optional[str]
|
||||||
) -> OAuth2Credentials:
|
) -> OAuth2Credentials:
|
||||||
# --8<-- [end:BaseOAuthHandler4]
|
# --8<-- [end:BaseOAuthHandler4]
|
||||||
"""Exchanges the acquired authorization code from login for a set of tokens"""
|
"""Exchanges the acquired authorization code from login for a set of tokens"""
|
||||||
|
|||||||
@@ -34,7 +34,9 @@ class GitHubOAuthHandler(BaseOAuthHandler):
|
|||||||
self.token_url = "https://github.com/login/oauth/access_token"
|
self.token_url = "https://github.com/login/oauth/access_token"
|
||||||
self.revoke_url = "https://api.github.com/applications/{client_id}/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 = {
|
params = {
|
||||||
"client_id": self.client_id,
|
"client_id": self.client_id,
|
||||||
"redirect_uri": self.redirect_uri,
|
"redirect_uri": self.redirect_uri,
|
||||||
@@ -44,7 +46,7 @@ class GitHubOAuthHandler(BaseOAuthHandler):
|
|||||||
return f"{self.auth_base_url}?{urlencode(params)}"
|
return f"{self.auth_base_url}?{urlencode(params)}"
|
||||||
|
|
||||||
def exchange_code_for_tokens(
|
def exchange_code_for_tokens(
|
||||||
self, code: str, scopes: list[str]
|
self, code: str, scopes: list[str], code_verifier: Optional[str]
|
||||||
) -> OAuth2Credentials:
|
) -> OAuth2Credentials:
|
||||||
return self._request_tokens({"code": code, "redirect_uri": self.redirect_uri})
|
return self._request_tokens({"code": code, "redirect_uri": self.redirect_uri})
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import logging
|
import logging
|
||||||
|
from typing import Optional
|
||||||
|
|
||||||
from google.auth.external_account_authorized_user import (
|
from google.auth.external_account_authorized_user import (
|
||||||
Credentials as ExternalAccountCredentials,
|
Credentials as ExternalAccountCredentials,
|
||||||
@@ -38,7 +39,9 @@ class GoogleOAuthHandler(BaseOAuthHandler):
|
|||||||
self.token_uri = "https://oauth2.googleapis.com/token"
|
self.token_uri = "https://oauth2.googleapis.com/token"
|
||||||
self.revoke_uri = "https://oauth2.googleapis.com/revoke"
|
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))
|
all_scopes = list(set(scopes + self.DEFAULT_SCOPES))
|
||||||
logger.debug(f"Setting up OAuth flow with scopes: {all_scopes}")
|
logger.debug(f"Setting up OAuth flow with scopes: {all_scopes}")
|
||||||
flow = self._setup_oauth_flow(all_scopes)
|
flow = self._setup_oauth_flow(all_scopes)
|
||||||
@@ -52,7 +55,7 @@ class GoogleOAuthHandler(BaseOAuthHandler):
|
|||||||
return authorization_url
|
return authorization_url
|
||||||
|
|
||||||
def exchange_code_for_tokens(
|
def exchange_code_for_tokens(
|
||||||
self, code: str, scopes: list[str]
|
self, code: str, scopes: list[str], code_verifier: Optional[str]
|
||||||
) -> OAuth2Credentials:
|
) -> OAuth2Credentials:
|
||||||
logger.debug(f"Exchanging code for tokens with scopes: {scopes}")
|
logger.debug(f"Exchanging code for tokens with scopes: {scopes}")
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
from base64 import b64encode
|
from base64 import b64encode
|
||||||
|
from typing import Optional
|
||||||
from urllib.parse import urlencode
|
from urllib.parse import urlencode
|
||||||
|
|
||||||
from backend.data.model import OAuth2Credentials
|
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.auth_base_url = "https://api.notion.com/v1/oauth/authorize"
|
||||||
self.token_url = "https://api.notion.com/v1/oauth/token"
|
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 = {
|
params = {
|
||||||
"client_id": self.client_id,
|
"client_id": self.client_id,
|
||||||
"redirect_uri": self.redirect_uri,
|
"redirect_uri": self.redirect_uri,
|
||||||
@@ -37,7 +40,7 @@ class NotionOAuthHandler(BaseOAuthHandler):
|
|||||||
return f"{self.auth_base_url}?{urlencode(params)}"
|
return f"{self.auth_base_url}?{urlencode(params)}"
|
||||||
|
|
||||||
def exchange_code_for_tokens(
|
def exchange_code_for_tokens(
|
||||||
self, code: str, scopes: list[str]
|
self, code: str, scopes: list[str], code_verifier: Optional[str]
|
||||||
) -> OAuth2Credentials:
|
) -> OAuth2Credentials:
|
||||||
request_body = {
|
request_body = {
|
||||||
"grant_type": "authorization_code",
|
"grant_type": "authorization_code",
|
||||||
|
|||||||
171
autogpt_platform/backend/backend/integrations/oauth/twitter.py
Normal file
171
autogpt_platform/backend/backend/integrations/oauth/twitter.py
Normal file
@@ -0,0 +1,171 @@
|
|||||||
|
import time
|
||||||
|
import urllib.parse
|
||||||
|
from typing import ClassVar, Optional
|
||||||
|
|
||||||
|
import requests
|
||||||
|
|
||||||
|
from backend.data.model import OAuth2Credentials, ProviderName
|
||||||
|
from backend.integrations.oauth.base import BaseOAuthHandler
|
||||||
|
|
||||||
|
|
||||||
|
class TwitterOAuthHandler(BaseOAuthHandler):
|
||||||
|
PROVIDER_NAME = ProviderName.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_challenge 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
|
||||||
@@ -27,5 +27,6 @@ class ProviderName(str, Enum):
|
|||||||
REPLICATE = "replicate"
|
REPLICATE = "replicate"
|
||||||
REVID = "revid"
|
REVID = "revid"
|
||||||
SLANT3D = "slant3d"
|
SLANT3D = "slant3d"
|
||||||
|
TWITTER = "twitter"
|
||||||
UNREAL_SPEECH = "unreal_speech"
|
UNREAL_SPEECH = "unreal_speech"
|
||||||
# --8<-- [end:ProviderName]
|
# --8<-- [end:ProviderName]
|
||||||
|
|||||||
@@ -60,11 +60,12 @@ def login(
|
|||||||
requested_scopes = scopes.split(",") if scopes else []
|
requested_scopes = scopes.split(",") if scopes else []
|
||||||
|
|
||||||
# Generate and store a secure random state token along with the scopes
|
# 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
|
user_id, provider, requested_scopes
|
||||||
)
|
)
|
||||||
|
login_url = handler.get_login_url(
|
||||||
login_url = handler.get_login_url(requested_scopes, state_token)
|
requested_scopes, state_token, code_challenge=code_challenge
|
||||||
|
)
|
||||||
|
|
||||||
return LoginResponse(login_url=login_url, state_token=state_token)
|
return LoginResponse(login_url=login_url, state_token=state_token)
|
||||||
|
|
||||||
@@ -92,19 +93,21 @@ def callback(
|
|||||||
handler = _get_provider_oauth_handler(request, provider)
|
handler = _get_provider_oauth_handler(request, provider)
|
||||||
|
|
||||||
# Verify the state token
|
# Verify the state token
|
||||||
if not creds_manager.store.verify_state_token(user_id, state_token, provider):
|
valid_state = creds_manager.store.verify_state_token(user_id, state_token, provider)
|
||||||
|
|
||||||
|
if not valid_state:
|
||||||
logger.warning(f"Invalid or expired state token for user {user_id}")
|
logger.warning(f"Invalid or expired state token for user {user_id}")
|
||||||
raise HTTPException(status_code=400, detail="Invalid or expired state token")
|
raise HTTPException(status_code=400, detail="Invalid or expired state token")
|
||||||
|
|
||||||
try:
|
try:
|
||||||
scopes = creds_manager.store.get_any_valid_scopes_from_state_token(
|
scopes = valid_state.scopes
|
||||||
user_id, state_token, provider
|
|
||||||
)
|
|
||||||
logger.debug(f"Retrieved scopes from state token: {scopes}")
|
logger.debug(f"Retrieved scopes from state token: {scopes}")
|
||||||
|
|
||||||
scopes = handler.handle_default_scopes(scopes)
|
scopes = handler.handle_default_scopes(scopes)
|
||||||
|
|
||||||
credentials = handler.exchange_code_for_tokens(code, scopes)
|
credentials = handler.exchange_code_for_tokens(
|
||||||
|
code, scopes, valid_state.code_verifier
|
||||||
|
)
|
||||||
|
|
||||||
logger.debug(f"Received credentials with final scopes: {credentials.scopes}")
|
logger.debug(f"Received credentials with final scopes: {credentials.scopes}")
|
||||||
|
|
||||||
# Check if the granted scopes are sufficient for the requested scopes
|
# Check if the granted scopes are sufficient for the requested scopes
|
||||||
|
|||||||
@@ -259,6 +259,10 @@ class Secrets(UpdateTrackingModel["Secrets"], BaseSettings):
|
|||||||
notion_client_secret: str = Field(
|
notion_client_secret: str = Field(
|
||||||
default="", description="Notion OAuth client secret"
|
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")
|
openai_api_key: str = Field(default="", description="OpenAI API key")
|
||||||
anthropic_api_key: str = Field(default="", description="Anthropic API key")
|
anthropic_api_key: str = Field(default="", description="Anthropic API key")
|
||||||
|
|||||||
1787
autogpt_platform/backend/poetry.lock
generated
1787
autogpt_platform/backend/poetry.lock
generated
File diff suppressed because it is too large
Load Diff
@@ -42,6 +42,7 @@ sentry-sdk = "2.19.2"
|
|||||||
strenum = "^0.4.9"
|
strenum = "^0.4.9"
|
||||||
supabase = "^2.10.0"
|
supabase = "^2.10.0"
|
||||||
tenacity = "^9.0.0"
|
tenacity = "^9.0.0"
|
||||||
|
tweepy = "^4.14.0"
|
||||||
uvicorn = { extras = ["standard"], version = "^0.34.0" }
|
uvicorn = { extras = ["standard"], version = "^0.34.0" }
|
||||||
websockets = "^13.1"
|
websockets = "^13.1"
|
||||||
youtube-transcript-api = "^0.6.2"
|
youtube-transcript-api = "^0.6.2"
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
/* flow.css or index.css */
|
/* flow.css or index.css */
|
||||||
|
|
||||||
body {
|
body {
|
||||||
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "Roboto",
|
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "Roboto", "Oxygen",
|
||||||
"Oxygen", "Ubuntu", "Cantarell", "Fira Sans", "Droid Sans",
|
"Ubuntu", "Cantarell", "Fira Sans", "Droid Sans", "Helvetica Neue",
|
||||||
"Helvetica Neue", sans-serif;
|
sans-serif;
|
||||||
}
|
}
|
||||||
|
|
||||||
code {
|
code {
|
||||||
|
|||||||
@@ -7,8 +7,15 @@ import SchemaTooltip from "@/components/SchemaTooltip";
|
|||||||
import useCredentials from "@/hooks/useCredentials";
|
import useCredentials from "@/hooks/useCredentials";
|
||||||
import { zodResolver } from "@hookform/resolvers/zod";
|
import { zodResolver } from "@hookform/resolvers/zod";
|
||||||
import { NotionLogoIcon } from "@radix-ui/react-icons";
|
import { NotionLogoIcon } from "@radix-ui/react-icons";
|
||||||
import { FaDiscord, FaGithub, FaGoogle, FaMedium, FaKey } from "react-icons/fa";
|
import {
|
||||||
import { FC, useState } from "react";
|
FaDiscord,
|
||||||
|
FaGithub,
|
||||||
|
FaTwitter,
|
||||||
|
FaGoogle,
|
||||||
|
FaMedium,
|
||||||
|
FaKey,
|
||||||
|
} from "react-icons/fa";
|
||||||
|
import { FC, useMemo, useState } from "react";
|
||||||
import {
|
import {
|
||||||
CredentialsMetaInput,
|
CredentialsMetaInput,
|
||||||
CredentialsProviderName,
|
CredentialsProviderName,
|
||||||
@@ -68,6 +75,7 @@ export const providerIcons: Record<
|
|||||||
replicate: fallbackIcon,
|
replicate: fallbackIcon,
|
||||||
fal: fallbackIcon,
|
fal: fallbackIcon,
|
||||||
revid: fallbackIcon,
|
revid: fallbackIcon,
|
||||||
|
twitter: FaTwitter,
|
||||||
unreal_speech: fallbackIcon,
|
unreal_speech: fallbackIcon,
|
||||||
exa: fallbackIcon,
|
exa: fallbackIcon,
|
||||||
hubspot: fallbackIcon,
|
hubspot: fallbackIcon,
|
||||||
|
|||||||
@@ -37,6 +37,7 @@ const providerDisplayNames: Record<CredentialsProviderName, string> = {
|
|||||||
replicate: "Replicate",
|
replicate: "Replicate",
|
||||||
fal: "FAL",
|
fal: "FAL",
|
||||||
revid: "Rev.ID",
|
revid: "Rev.ID",
|
||||||
|
twitter: "Twitter",
|
||||||
unreal_speech: "Unreal Speech",
|
unreal_speech: "Unreal Speech",
|
||||||
exa: "Exa",
|
exa: "Exa",
|
||||||
hubspot: "Hubspot",
|
hubspot: "Hubspot",
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ import {
|
|||||||
BlockIOStringSubSchema,
|
BlockIOStringSubSchema,
|
||||||
BlockIONumberSubSchema,
|
BlockIONumberSubSchema,
|
||||||
BlockIOBooleanSubSchema,
|
BlockIOBooleanSubSchema,
|
||||||
|
BlockIOSimpleTypeSubSchema,
|
||||||
} from "@/lib/autogpt-server-api/types";
|
} from "@/lib/autogpt-server-api/types";
|
||||||
import React, { FC, useCallback, useEffect, useMemo, useState } from "react";
|
import React, { FC, useCallback, useEffect, useMemo, useState } from "react";
|
||||||
import { Button } from "./ui/button";
|
import { Button } from "./ui/button";
|
||||||
@@ -40,6 +41,7 @@ import { LocalValuedInput } from "./ui/input";
|
|||||||
import NodeHandle from "./NodeHandle";
|
import NodeHandle from "./NodeHandle";
|
||||||
import { ConnectionData } from "./CustomNode";
|
import { ConnectionData } from "./CustomNode";
|
||||||
import { CredentialsInput } from "./integrations/credentials-input";
|
import { CredentialsInput } from "./integrations/credentials-input";
|
||||||
|
import { MultiSelect } from "./ui/multiselect-input";
|
||||||
|
|
||||||
type NodeObjectInputTreeProps = {
|
type NodeObjectInputTreeProps = {
|
||||||
nodeId: string;
|
nodeId: string;
|
||||||
@@ -295,7 +297,25 @@ export const NodeGenericInputField: FC<{
|
|||||||
"type" in s ? s.type : undefined,
|
"type" in s ? s.type : undefined,
|
||||||
);
|
);
|
||||||
if (types.includes("string") && types.includes("null")) {
|
if (types.includes("string") && types.includes("null")) {
|
||||||
// optional string
|
// optional string and datetime
|
||||||
|
|
||||||
|
if (
|
||||||
|
"format" in propSchema.anyOf[0] &&
|
||||||
|
propSchema.anyOf[0].format === "date-time"
|
||||||
|
) {
|
||||||
|
return (
|
||||||
|
<NodeDateTimeInput
|
||||||
|
selfKey={propKey}
|
||||||
|
schema={propSchema.anyOf[0]}
|
||||||
|
value={currentValue}
|
||||||
|
error={errors[propKey]}
|
||||||
|
className={className}
|
||||||
|
displayName={displayName}
|
||||||
|
handleInputChange={handleInputChange}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<NodeStringInput
|
<NodeStringInput
|
||||||
selfKey={propKey}
|
selfKey={propKey}
|
||||||
@@ -356,6 +376,42 @@ export const NodeGenericInputField: FC<{
|
|||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
} else if (types.includes("object") && types.includes("null")) {
|
} else if (types.includes("object") && types.includes("null")) {
|
||||||
|
// rendering optional mutliselect
|
||||||
|
if (
|
||||||
|
Object.values(
|
||||||
|
(propSchema.anyOf[0] as BlockIOObjectSubSchema).properties,
|
||||||
|
).every(
|
||||||
|
(subSchema) => "type" in subSchema && subSchema.type == "boolean",
|
||||||
|
) &&
|
||||||
|
Object.keys((propSchema.anyOf[0] as BlockIOObjectSubSchema).properties)
|
||||||
|
.length >= 3
|
||||||
|
) {
|
||||||
|
const options = Object.keys(
|
||||||
|
(propSchema.anyOf[0] as BlockIOObjectSubSchema).properties,
|
||||||
|
);
|
||||||
|
const selectedKeys = Object.entries(currentValue || {})
|
||||||
|
.filter(([_, v]) => v)
|
||||||
|
.map(([k, _]) => k);
|
||||||
|
return (
|
||||||
|
<NodeMultiSelectInput
|
||||||
|
selfKey={propKey}
|
||||||
|
schema={propSchema.anyOf[0] as BlockIOObjectSubSchema}
|
||||||
|
selection={selectedKeys}
|
||||||
|
error={errors[propKey]}
|
||||||
|
className={className}
|
||||||
|
displayName={displayName}
|
||||||
|
handleInputChange={(key, selection) => {
|
||||||
|
handleInputChange(
|
||||||
|
key,
|
||||||
|
Object.fromEntries(
|
||||||
|
options.map((option) => [option, selection.includes(option)]),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<NodeKeyValueInput
|
<NodeKeyValueInput
|
||||||
nodeId={nodeId}
|
nodeId={nodeId}
|
||||||
@@ -826,6 +882,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<{
|
const NodeArrayInput: FC<{
|
||||||
nodeId: string;
|
nodeId: string;
|
||||||
selfKey: string;
|
selfKey: string;
|
||||||
|
|||||||
@@ -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";
|
||||||
@@ -41,7 +41,7 @@ export type BlockIOSubSchema =
|
|||||||
| BlockIOSimpleTypeSubSchema
|
| BlockIOSimpleTypeSubSchema
|
||||||
| BlockIOCombinedTypeSubSchema;
|
| BlockIOCombinedTypeSubSchema;
|
||||||
|
|
||||||
type BlockIOSimpleTypeSubSchema =
|
export type BlockIOSimpleTypeSubSchema =
|
||||||
| BlockIOObjectSubSchema
|
| BlockIOObjectSubSchema
|
||||||
| BlockIOCredentialsSubSchema
|
| BlockIOCredentialsSubSchema
|
||||||
| BlockIOKVSubSchema
|
| BlockIOKVSubSchema
|
||||||
@@ -125,6 +125,7 @@ export const PROVIDER_NAMES = {
|
|||||||
UNREAL_SPEECH: "unreal_speech",
|
UNREAL_SPEECH: "unreal_speech",
|
||||||
EXA: "exa",
|
EXA: "exa",
|
||||||
HUBSPOT: "hubspot",
|
HUBSPOT: "hubspot",
|
||||||
|
TWITTER: "twitter",
|
||||||
} as const;
|
} as const;
|
||||||
// --8<-- [end:BlockIOCredentialsSubSchema]
|
// --8<-- [end:BlockIOCredentialsSubSchema]
|
||||||
|
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user