updated blocks and cred store access

This commit is contained in:
SwiftyOS
2025-05-16 10:13:40 +02:00
parent cae4a6e145
commit 2d3842bdd1
3 changed files with 334 additions and 125 deletions

View File

@@ -2,15 +2,15 @@ from typing import Literal
from pydantic import SecretStr
from backend.data.model import APIKeyCredentials, CredentialsMetaInput
from backend.data.model import APIKeyCredentials, CredentialsField, CredentialsMetaInput
from backend.integrations.providers import ProviderName
# Define the type of credentials input expected for Example API
ExampleCredentialsInput = CredentialsMetaInput[
AyrshareCredentials = APIKeyCredentials
AyrshareCredentialsInput = CredentialsMetaInput[
Literal[ProviderName.AYRSHARE], Literal["api_key"]
]
# Mock credentials for testing Example API integration
AYRSHARE_CREDENTIALS = APIKeyCredentials(
id="8274be87-67a7-445d-bbc3-d27a017100f0",
@@ -27,3 +27,8 @@ AYRSHARE_CREDENTIALS_INPUT = {
"type": AYRSHARE_CREDENTIALS.type,
"title": AYRSHARE_CREDENTIALS.title,
}
def AyrshareCredentialsField() -> AyrshareCredentialsInput:
"""Creates an Ayrshare credentials input on a block."""
return CredentialsField(description="The Ayrshare integration requires an API Key.")

View File

@@ -13,57 +13,16 @@ from backend.blocks.aryshare._api import (
)
from backend.blocks.aryshare._auth import AYRSHARE_CREDENTIALS
from backend.data.block import Block, BlockCategory, BlockOutput, BlockSchema
from backend.data.model import APIKeyCredentials
from backend.data.model import APIKeyCredentials, SchemaField
from backend.integrations.credentials_store import IntegrationCredentialsStore
from ._auth import AYRSHARE_CREDENTIALS_INPUT
from ._auth import AyrshareCredentialsField, AyrshareCredentialsInput
logger = logging.getLogger(__name__)
creads_store = IntegrationCredentialsStore()
class AyrsharePostInput(BlockSchema):
"""Base input model for Ayrshare social media posts."""
post: str = Field(..., description="The post text to be published")
media_urls: Optional[List[str]] = Field(
None, description="Optional list of media URLs to include"
)
is_video: Optional[bool] = Field(None, description="Whether the media is a video")
schedule_date: Optional[str] = Field(
None, description="UTC datetime for scheduling (YYYY-MM-DDThh:mm:ssZ)"
)
first_comment: Optional[FirstComment] = Field(
None, description="Configuration for first comment"
)
disable_comments: Optional[bool] = Field(
None, description="Whether to disable comments"
)
shorten_links: Optional[bool] = Field(None, description="Whether to shorten links")
auto_schedule: Optional[AutoSchedule] = Field(
None, description="Configuration for automatic scheduling"
)
auto_repost: Optional[AutoRepost] = Field(
None, description="Configuration for automatic reposting"
)
auto_hashtag: Optional[Union[AutoHashtag, bool]] = Field(
None, description="Configuration for automatic hashtags"
)
unsplash: Optional[str] = Field(None, description="Unsplash image configuration")
requires_approval: Optional[bool] = Field(
None, description="Whether to enable approval workflow"
)
random_post: Optional[bool] = Field(
None, description="Whether to generate random post text"
)
random_media_url: Optional[bool] = Field(
None, description="Whether to generate random media"
)
idempotency_key: Optional[str] = Field(None, description="Unique ID for the post")
notes: Optional[str] = Field(None, description="Additional notes for the post")
class RequestOutput(BaseModel):
"""Base output model for Ayrshare social media posts."""
@@ -73,34 +32,120 @@ class RequestOutput(BaseModel):
profileTitle: str = Field(..., description="Title of the profile")
post: str = Field(..., description="The post text")
postIds: Optional[List[dict]] = Field(
None, description="IDs of the posts on each platform"
description="IDs of the posts on each platform"
)
scheduleDate: Optional[str] = Field(None, description="Scheduled date of the post")
errors: Optional[List[str]] = Field(None, description="Any errors that occurred")
class AyrsharePostOutput(BlockSchema):
post_result: RequestOutput = Field(..., description="The result of the post")
scheduleDate: Optional[str] = Field(description="Scheduled date of the post")
errors: Optional[List[str]] = Field(description="Any errors that occurred")
class BaseAyrsharePostBlock(Block):
"""Base class for Ayrshare social media posting blocks."""
def __init__(self):
class Input(BlockSchema):
"""Base input model for Ayrshare social media posts."""
credentials: AyrshareCredentialsInput = AyrshareCredentialsField()
post: str = SchemaField(
description="The post text to be published", default="", advanced=False
)
media_urls: Optional[List[str]] = SchemaField(
description="Optional list of media URLs to include",
default=None,
advanced=False,
)
is_video: Optional[bool] = SchemaField(
description="Whether the media is a video", default=None, advanced=False
)
schedule_date: Optional[str] = SchemaField(
description="UTC datetime for scheduling (YYYY-MM-DDThh:mm:ssZ)",
default=None,
advanced=False,
)
first_comment: Optional[FirstComment] = SchemaField(
description="Configuration for first comment", default=None, advanced=False
)
disable_comments: Optional[bool] = SchemaField(
description="Whether to disable comments", default=None, advanced=False
)
shorten_links: Optional[bool] = SchemaField(
description="Whether to shorten links", default=None, advanced=False
)
auto_schedule: Optional[AutoSchedule] = SchemaField(
description="Configuration for automatic scheduling",
default=None,
advanced=False,
)
auto_repost: Optional[AutoRepost] = SchemaField(
description="Configuration for automatic reposting",
default=None,
advanced=False,
)
auto_hashtag: Optional[Union[AutoHashtag, bool]] = SchemaField(
description="Configuration for automatic hashtags",
default=None,
advanced=False,
)
unsplash: Optional[str] = SchemaField(
description="Unsplash image configuration", default=None, advanced=False
)
requires_approval: Optional[bool] = SchemaField(
description="Whether to enable approval workflow",
default=None,
advanced=False,
)
random_post: Optional[bool] = SchemaField(
description="Whether to generate random post text",
default=None,
advanced=False,
)
random_media_url: Optional[bool] = SchemaField(
description="Whether to generate random media", default=None, advanced=False
)
idempotency_key: Optional[str] = SchemaField(
description="Unique ID for the post", default=None, advanced=False
)
notes: Optional[str] = SchemaField(
description="Additional notes for the post", default=None, advanced=False
)
class Output(BlockSchema):
post_result: RequestOutput = SchemaField(description="The result of the post")
def __init__(
self,
id="b3a7b3b9-5169-410a-9d5c-fd625460fb14",
description="Ayrshare Post",
test_output=[
(
"post_result",
RequestOutput(
status="success",
id="12345",
refId="abc123",
profileTitle="Test Profile",
post="Hello, world! This is a test post.",
postIds=[{"platform": "facebook", "id": "fb_123456"}],
scheduleDate=None,
errors=None,
),
),
],
):
super().__init__(
# The unique identifier for the block, this value will be persisted in the DB.
# It should be unique and constant across the application run.
# Use the UUID format for the ID.
id="380694d5-3b2e-4130-bced-b43752b70de9",
id=id,
# The description of the block, explaining what the block does.
description="Base class for Ayrshare social media posting blocks",
description=description,
# The set of categories that the block belongs to.
# Each category is an instance of BlockCategory Enum.
categories={BlockCategory.SOCIAL},
# The schema, defined as a Pydantic model, for the input data.
input_schema=AyrsharePostInput,
input_schema=BaseAyrsharePostBlock.Input,
# The schema, defined as a Pydantic model, for the output data.
output_schema=AyrsharePostOutput,
output_schema=BaseAyrsharePostBlock.Output,
# The credentials required for testing the block.
# This is an instance of APIKeyCredentials with sample values.
test_credentials=AYRSHARE_CREDENTIALS,
@@ -110,25 +155,16 @@ class BaseAyrsharePostBlock(Block):
"post": "Hello, world! This is a test post.",
"media_urls": ["https://example.com/image.jpg"],
"is_video": False,
"credentials": AYRSHARE_CREDENTIALS_INPUT,
"credentials": {
"provider": "ayrshare",
"id": AYRSHARE_CREDENTIALS.id,
"type": AYRSHARE_CREDENTIALS.type,
"title": AYRSHARE_CREDENTIALS.title,
},
},
# The list or single expected output if the test_input is run.
# Each output is a tuple of (output_name, output_data).
test_output=[
(
"post_result",
RequestOutput(
status="success",
id="12345",
refId="abc123",
profileTitle="Test Profile",
post="Hello, world! This is a test post.",
postIds=[{"platform": "facebook", "id": "fb_123456"}],
scheduleDate=None,
errors=None,
),
),
],
test_output=test_output,
# Function names on the block implementation to mock on test run.
# Each mock is a dictionary with function names as keys and mock implementations as values.
test_mock={
@@ -141,7 +177,8 @@ class BaseAyrsharePostBlock(Block):
postIds=[{"platform": "facebook", "id": "fb_123456"}],
scheduleDate=None,
errors=None,
)
),
"_get_profile_key": lambda user_id: ("profile_key", "mock_profile_key"),
},
)
@@ -153,11 +190,11 @@ class BaseAyrsharePostBlock(Block):
def _create_post(
self,
input_data: AyrsharePostInput,
input_data: "BaseAyrsharePostBlock.Input",
platforms: List[SocialPlatform],
profile_key: Optional[str] = None,
credentials: Optional[APIKeyCredentials] = None,
) -> AyrsharePostOutput:
) -> RequestOutput:
client = self.create_client(credentials)
"""Create a post on the specified platforms."""
response = client.create_post(
@@ -180,47 +217,61 @@ class BaseAyrsharePostBlock(Block):
notes=input_data.notes,
profile_key=profile_key,
)
return AyrsharePostOutput(**response.__dict__)
return RequestOutput(**response.__dict__)
def _get_profile_key(self, user_id: str) -> tuple[str, str]:
creds_store = IntegrationCredentialsStore()
profile_key = creds_store.get_ayrshare_profile_key(user_id)
if profile_key:
return "profile_key", profile_key.get_secret_value()
else:
return (
"error",
"You need to connect your social media profile to Ayrshare first.",
)
def run(
self,
input_data: AyrsharePostInput,
input_data: "BaseAyrsharePostBlock.Input",
*,
credentials: APIKeyCredentials,
**kwargs,
) -> BlockOutput:
"""Run the block."""
platforms = [SocialPlatform.FACEBOOK]
yield "post_result", self._create_post(input_data, platforms=platforms, credentials=credentials)
yield "post_result", self._create_post(
input_data, platforms=platforms, credentials=credentials
)
class PostToFacebookBlock(BaseAyrsharePostBlock):
"""Block for posting to Facebook."""
def __init__(self):
super().__init__()
super().__init__(
id="3352f512-3524-49ed-a08f-003042da2fc1",
description="Post to Facebook",
)
def run(
self,
input_data: AyrsharePostInput,
input_data: BaseAyrsharePostBlock.Input,
*,
user_id: str,
credentials: APIKeyCredentials,
**kwargs,
) -> BlockOutput:
"""Post to Facebook."""
creds_store = IntegrationCredentialsStore()
profile_key = creds_store.get_ayrshare_profile_key(user_id)
if profile_key:
logger.info(f"Profile key: {profile_key}")
else:
yield "error", "Profile key not found"
profile_key, profile_key_value = self._get_profile_key(user_id)
if profile_key == "error":
yield "error", profile_key_value
return
post_result = self._create_post(
input_data,
[SocialPlatform.FACEBOOK],
profile_key=profile_key.get_secret_value() if profile_key else None,
profile_key=profile_key_value,
credentials=credentials,
)
yield "post_result", post_result
@@ -230,14 +281,30 @@ class PostToTwitterBlock(BaseAyrsharePostBlock):
"""Block for posting to Twitter."""
def __init__(self):
super().__init__()
super().__init__(
id="9e8f844e-b4a5-4b25-80f2-9e1dd7d67625",
description="Post to Twitter",
)
def run(
self, input_data: AyrsharePostInput, *, credentials: APIKeyCredentials, **kwargs
self,
input_data: BaseAyrsharePostBlock.Input,
*,
user_id: str,
credentials: APIKeyCredentials,
**kwargs,
) -> BlockOutput:
"""Post to Twitter."""
profile_key, profile_key_value = self._get_profile_key(user_id)
if profile_key == "error":
yield "error", profile_key_value
return
post_result = self._create_post(
input_data, [SocialPlatform.TWITTER], credentials=credentials
input_data,
[SocialPlatform.TWITTER],
profile_key=profile_key_value,
credentials=credentials,
)
yield "post_result", post_result
@@ -246,14 +313,30 @@ class PostToLinkedInBlock(BaseAyrsharePostBlock):
"""Block for posting to LinkedIn."""
def __init__(self):
super().__init__()
super().__init__(
id="589af4e4-507f-42fd-b9ac-a67ecef25811",
description="Post to LinkedIn",
)
def run(
self, input_data: AyrsharePostInput, *, credentials: APIKeyCredentials, **kwargs
self,
input_data: BaseAyrsharePostBlock.Input,
*,
user_id: str,
credentials: APIKeyCredentials,
**kwargs,
) -> BlockOutput:
"""Post to LinkedIn."""
profile_key, profile_key_value = self._get_profile_key(user_id)
if profile_key == "error":
yield "error", profile_key_value
return
post_result = self._create_post(
input_data, [SocialPlatform.LINKEDIN], credentials=credentials
input_data,
[SocialPlatform.LINKEDIN],
profile_key=profile_key_value,
credentials=credentials,
)
yield "post_result", post_result
@@ -262,14 +345,30 @@ class PostToInstagramBlock(BaseAyrsharePostBlock):
"""Block for posting to Instagram."""
def __init__(self):
super().__init__()
super().__init__(
id="89b02b96-a7cb-46f4-9900-c48b32fe1552",
description="Post to Instagram",
)
def run(
self, input_data: AyrsharePostInput, *, credentials: APIKeyCredentials, **kwargs
self,
input_data: BaseAyrsharePostBlock.Input,
*,
user_id: str,
credentials: APIKeyCredentials,
**kwargs,
) -> BlockOutput:
"""Post to Instagram."""
profile_key, profile_key_value = self._get_profile_key(user_id)
if profile_key == "error":
yield "error", profile_key_value
return
post_result = self._create_post(
input_data, [SocialPlatform.INSTAGRAM], credentials=credentials
input_data,
[SocialPlatform.INSTAGRAM],
profile_key=profile_key_value,
credentials=credentials,
)
yield "post_result", post_result
@@ -278,14 +377,30 @@ class PostToYouTubeBlock(BaseAyrsharePostBlock):
"""Block for posting to YouTube."""
def __init__(self):
super().__init__()
super().__init__(
id="0082d712-ff1b-4c3d-8a8d-6c7721883b83",
description="Post to YouTube",
)
def run(
self, input_data: AyrsharePostInput, *, credentials: APIKeyCredentials, **kwargs
self,
input_data: BaseAyrsharePostBlock.Input,
*,
user_id: str,
credentials: APIKeyCredentials,
**kwargs,
) -> BlockOutput:
"""Post to YouTube."""
profile_key, profile_key_value = self._get_profile_key(user_id)
if profile_key == "error":
yield "error", profile_key_value
return
post_result = self._create_post(
input_data, [SocialPlatform.YOUTUBE], credentials=credentials
input_data,
[SocialPlatform.YOUTUBE],
profile_key=profile_key_value,
credentials=credentials,
)
yield "post_result", post_result
@@ -294,14 +409,30 @@ class PostToRedditBlock(BaseAyrsharePostBlock):
"""Block for posting to Reddit."""
def __init__(self):
super().__init__()
super().__init__(
id="c7733580-3c72-483e-8e47-a8d58754d853",
description="Post to Reddit",
)
def run(
self, input_data: AyrsharePostInput, *, credentials: APIKeyCredentials, **kwargs
self,
input_data: BaseAyrsharePostBlock.Input,
*,
user_id: str,
credentials: APIKeyCredentials,
**kwargs,
) -> BlockOutput:
"""Post to Reddit."""
profile_key, profile_key_value = self._get_profile_key(user_id)
if profile_key == "error":
yield "error", profile_key_value
return
post_result = self._create_post(
input_data, [SocialPlatform.REDDIT], credentials=credentials
input_data,
[SocialPlatform.REDDIT],
profile_key=profile_key_value,
credentials=credentials,
)
yield "post_result", post_result
@@ -310,14 +441,30 @@ class PostToTelegramBlock(BaseAyrsharePostBlock):
"""Block for posting to Telegram."""
def __init__(self):
super().__init__()
super().__init__(
id="47bc74eb-4af2-452c-b933-af377c7287df",
description="Post to Telegram",
)
def run(
self, input_data: AyrsharePostInput, *, credentials: APIKeyCredentials, **kwargs
self,
input_data: BaseAyrsharePostBlock.Input,
*,
user_id: str,
credentials: APIKeyCredentials,
**kwargs,
) -> BlockOutput:
"""Post to Telegram."""
profile_key, profile_key_value = self._get_profile_key(user_id)
if profile_key == "error":
yield "error", profile_key_value
return
post_result = self._create_post(
input_data, [SocialPlatform.TELEGRAM], credentials=credentials
input_data,
[SocialPlatform.TELEGRAM],
profile_key=profile_key_value,
credentials=credentials,
)
yield "post_result", post_result
@@ -326,14 +473,30 @@ class PostToGMBBlock(BaseAyrsharePostBlock):
"""Block for posting to Google My Business."""
def __init__(self):
super().__init__()
super().__init__(
id="2c38c783-c484-4503-9280-ef5d1d345a7e",
description="Post to Google My Business",
)
def run(
self, input_data: AyrsharePostInput, *, credentials: APIKeyCredentials, **kwargs
self,
input_data: BaseAyrsharePostBlock.Input,
*,
user_id: str,
credentials: APIKeyCredentials,
**kwargs,
) -> BlockOutput:
"""Post to Google My Business."""
profile_key, profile_key_value = self._get_profile_key(user_id)
if profile_key == "error":
yield "error", profile_key_value
return
post_result = self._create_post(
input_data, [SocialPlatform.GMB], credentials=credentials
input_data,
[SocialPlatform.GMB],
profile_key=profile_key_value,
credentials=credentials,
)
yield "post_result", post_result
@@ -342,14 +505,30 @@ class PostToPinterestBlock(BaseAyrsharePostBlock):
"""Block for posting to Pinterest."""
def __init__(self):
super().__init__()
super().__init__(
id="3ca46e05-dbaa-4afb-9e95-5a429c4177e6",
description="Post to Pinterest",
)
def run(
self, input_data: AyrsharePostInput, *, credentials: APIKeyCredentials, **kwargs
self,
input_data: BaseAyrsharePostBlock.Input,
*,
user_id: str,
credentials: APIKeyCredentials,
**kwargs,
) -> BlockOutput:
"""Post to Pinterest."""
profile_key, profile_key_value = self._get_profile_key(user_id)
if profile_key == "error":
yield "error", profile_key_value
return
post_result = self._create_post(
input_data, [SocialPlatform.PINTEREST], credentials=credentials
input_data,
[SocialPlatform.PINTEREST],
profile_key=profile_key_value,
credentials=credentials,
)
yield "post_result", post_result
@@ -358,19 +537,30 @@ class PostToTikTokBlock(BaseAyrsharePostBlock):
"""Block for posting to TikTok."""
def __init__(self):
super().__init__()
super().__init__(
id="7faf4b27-96b0-4f05-bf64-e0de54ae74e1",
description="Post to TikTok",
)
def run(
self,
input_data: AyrsharePostInput,
input_data: BaseAyrsharePostBlock.Input,
*,
user_id: str,
credentials: APIKeyCredentials,
**kwargs,
) -> BlockOutput:
"""Post to TikTok."""
profile_key, profile_key_value = self._get_profile_key(user_id)
if profile_key == "error":
yield "error", profile_key_value
return
post_result = self._create_post(
input_data, [SocialPlatform.TIKTOK], credentials=credentials
input_data,
[SocialPlatform.TIKTOK],
profile_key=profile_key_value,
credentials=credentials,
)
yield "post_result", post_result
@@ -379,13 +569,29 @@ class PostToBlueskyBlock(BaseAyrsharePostBlock):
"""Block for posting to Bluesky."""
def __init__(self):
super().__init__()
super().__init__(
id="cbd52c2a-06d2-43ed-9560-6576cc163283",
description="Post to Bluesky",
)
def run(
self, input_data: AyrsharePostInput, *, credentials: APIKeyCredentials, **kwargs
self,
input_data: BaseAyrsharePostBlock.Input,
*,
user_id: str,
credentials: APIKeyCredentials,
**kwargs,
) -> BlockOutput:
"""Post to Bluesky."""
profile_key, profile_key_value = self._get_profile_key(user_id)
if profile_key == "error":
yield "error", profile_key_value
return
post_result = self._create_post(
input_data, [SocialPlatform.BLUESKY], credentials=credentials
input_data,
[SocialPlatform.BLUESKY],
profile_key=profile_key_value,
credentials=credentials,
)
yield "post_result", post_result

View File

@@ -18,7 +18,6 @@ from backend.data.integrations import (
)
from backend.data.model import Credentials, CredentialsType, OAuth2Credentials
from backend.executor.utils import add_graph_execution_async
from backend.integrations.credentials_store import IntegrationCredentialsStore
from backend.integrations.creds_manager import IntegrationCredentialsManager
from backend.integrations.oauth import HANDLERS_BY_NAME
from backend.integrations.providers import ProviderName
@@ -36,7 +35,6 @@ settings = Settings()
router = APIRouter()
creds_manager = IntegrationCredentialsManager()
creds_store = IntegrationCredentialsStore()
class LoginResponse(BaseModel):
@@ -434,13 +432,13 @@ async def get_ayrshare_sso_url(
"""
# Get or create profile key
profile_key = creds_store.get_ayrshare_profile_key(user_id)
profile_key = creds_manager.store.get_ayrshare_profile_key(user_id)
if not profile_key:
# Create new profile if none exists
client = AyrshareClient(api_key=settings.secrets.ayrshare_api_key)
profile = client.create_profile(title=f"User {user_id}", messaging_active=True)
profile_key = profile.profileKey
creds_store.set_ayrshare_profile_key(user_id, profile_key)
creds_manager.store.set_ayrshare_profile_key(user_id, profile_key)
# Convert SecretStr to string if needed
profile_key_str = (