Skip to content

Aryshare revid #9988

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Draft
wants to merge 36 commits into
base: dev
Choose a base branch
from
Draft

Aryshare revid #9988

wants to merge 36 commits into from

Conversation

majdyz
Copy link
Contributor

@majdyz majdyz commented May 20, 2025

Changes 🏗️

Checklist 📋

For code changes:

  • I have clearly listed my changes in the PR description
  • I have made a test plan
  • I have tested my changes according to the test plan:
    • ...
Example test plan
  • Create from scratch and execute an agent with at least 3 blocks
  • Import an agent from file upload, and confirm it executes correctly
  • Upload agent to marketplace
  • Import an agent from marketplace and confirm it executes correctly
  • Edit an agent from monitor, and confirm it executes correctly

For configuration changes:

  • .env.example is updated or already compatible with my changes
  • docker-compose.yml is updated or already compatible with my changes
  • I have included a list of my configuration changes in the PR description (under Changes)
Examples of configuration changes
  • Changing ports
  • Adding new services that need to communicate with each other
  • Secrets or environment variable changes
  • New or infrastructure changes such as databases

@majdyz majdyz requested a review from a team as a code owner May 20, 2025 13:23
@majdyz majdyz requested review from Swiftyos and Pwuts and removed request for a team May 20, 2025 13:23
@github-project-automation github-project-automation bot moved this to 🆕 Needs initial review in AutoGPT development kanban May 20, 2025
Copy link

supabase bot commented May 20, 2025

This pull request has been ignored for the connected project bgwpwdsxblryihinutbx because there are no changes detected in supabase directory. You can change this behaviour in Project Integrations Settings ↗︎.


Preview Branches by Supabase.
Learn more about Supabase Branching ↗︎.

Copy link
Contributor

This PR targets the master branch but does not come from dev or a hotfix/* branch.

Automatically setting the base branch to dev.

@github-actions github-actions bot added Classic Benchmark platform/frontend AutoGPT Platform - Front end platform/backend AutoGPT Platform - Back end platform/blocks labels May 20, 2025
@github-actions github-actions bot changed the base branch from master to dev May 20, 2025 13:23
Copy link

PR Reviewer Guide 🔍

Here are some key observations to aid the review process:

⏱️ Estimated effort to review: 4 🔵🔵🔵🔵⚪
🧪 No relevant tests
🔒 Security concerns

Sensitive information exposure:
The Ayrshare integration in server/integrations/router.py handles profile keys and JWT tokens. While SecretStr is used for storage, ensure that these values aren't accidentally logged or exposed in responses. Additionally, the error handling in the JWT generation could potentially leak implementation details if not properly sanitized.

⚡ Recommended focus areas for review

Code Duplication

The three new video creator blocks (AIAdMakerVideoCreatorBlock, AIPromptToVideoCreatorBlock, AIScreenshotToVideoAdBlock) share very similar structure and functionality. Consider refactoring to reduce duplication by extracting common payload construction logic.

class AIAdMakerVideoCreatorBlock(Block, _RevidMixin):
    """Generates a 30‑second vertical AI advert using optional user‑supplied imagery."""

    class Input(BlockSchema):
        credentials: CredentialsMetaInput[
            Literal[ProviderName.REVID], Literal["api_key"]
        ] = CredentialsField(
            description="Credentials for Revid.ai API access.",
        )
        script: str = SchemaField(
            description="Short advertising copy. Line breaks create new scenes.",
            placeholder="Introducing Foobar – [show product photo] the gadget that does it all.",
        )
        ratio: str = SchemaField(description="Aspect ratio", default="9 / 16")
        target_duration: int = SchemaField(
            description="Desired length of the ad in seconds.", default=30
        )
        voice: Voice = SchemaField(
            description="Narration voice", default=Voice.EVA, placeholder=Voice.EVA
        )
        background_music: AudioTrack = SchemaField(
            description="Background track",
            default=AudioTrack.DONT_STOP_ME_ABSTRACT_FUTURE_BASS,
        )
        input_media_urls: list[str] = SchemaField(
            description="List of image URLs to feature in the advert.", default=[]
        )
        use_only_provided_media: bool = SchemaField(
            description="Restrict visuals to supplied images only.", default=True
        )

    class Output(BlockSchema):
        video_url: str = SchemaField(description="URL of the finished advert")
        error: str = SchemaField(description="Error message on failure")

    def __init__(self):
        super().__init__(
            id="3e3fd845-000e-457f-9f50-9f2f9e278bbd",
            description="Creates an AI‑generated 30‑second advert (text + images)",
            categories={BlockCategory.MARKETING, BlockCategory.AI},
            input_schema=AIAdMakerVideoCreatorBlock.Input,
            output_schema=AIAdMakerVideoCreatorBlock.Output,
            test_input={
                "credentials": TEST_CREDENTIALS_INPUT,
                "script": "Test product launch!",
                "input_media_urls": [
                    "https://cdn.revid.ai/uploads/1747076315114-image.png",
                ],
            },
            test_output=("video_url", "https://example.com/ad.mp4"),
            test_mock={
                "create_video": lambda api_key, payload: {"pid": "test_pid"},
                "wait_for_video": lambda api_key, pid, max_wait_time=3600: "https://example.com/ad.mp4",
            },
            test_credentials=TEST_CREDENTIALS,
        )

    def run(self, input_data: Input, *, credentials: APIKeyCredentials, **kwargs):

        payload = {
            "webhook": None,
            "creationParams": {
                "targetDuration": input_data.target_duration,
                "ratio": input_data.ratio,
                "mediaType": "aiVideo",
                "inputText": input_data.script,
                "flowType": "text-to-video",
                "slug": "ai-ad-generator",
                "slugNew": "",
                "isCopiedFrom": False,
                "hasToGenerateVoice": True,
                "hasToTranscript": False,
                "hasToSearchMedia": True,
                "hasAvatar": False,
                "hasWebsiteRecorder": False,
                "hasTextSmallAtBottom": False,
                "selectedAudio": input_data.background_music.value,
                "selectedVoice": input_data.voice.voice_id,
                "selectedAvatar": "https://cdn.revid.ai/avatars/young-woman.mp4",
                "selectedAvatarType": "video/mp4",
                "websiteToRecord": "",
                "hasToGenerateCover": True,
                "nbGenerations": 1,
                "disableCaptions": False,
                "mediaMultiplier": "medium",
                "characters": [],
                "captionPresetName": "Revid",
                "sourceType": "contentScraping",
                "selectedStoryStyle": {"value": "custom", "label": "General"},
                "generationPreset": "DEFAULT",
                "hasToGenerateMusic": False,
                "isOptimizedForChinese": False,
                "generationUserPrompt": "",
                "enableNsfwFilter": False,
                "addStickers": False,
                "typeMovingImageAnim": "dynamic",
                "hasToGenerateSoundEffects": False,
                "forceModelType": "gpt-image-1",
                "selectedCharacters": [],
                "lang": "",
                "voiceSpeed": 1,
                "disableAudio": False,
                "disableVoice": False,
                "useOnlyProvidedMedia": input_data.use_only_provided_media,
                "imageGenerationModel": "ultra",
                "videoGenerationModel": "base",
                "hasEnhancedGeneration": True,
                "hasEnhancedGenerationPro": True,
                "inputMedias": [
                    {"url": url, "title": "", "type": "image"}
                    for url in input_data.input_media_urls
                ],
                "hasToGenerateVideos": True,
                "audioUrl": input_data.background_music.audio_url,
                "watermark": None,
            },
        }

        response = self.create_video(credentials.api_key, payload)
        pid = response.get("pid")
        if not pid:
            raise RuntimeError("Failed to create video: No project ID returned")

        video_url = self.wait_for_video(credentials.api_key, pid)
        yield "video_url", video_url


class AIPromptToVideoCreatorBlock(Block, _RevidMixin):
    """Turns a single creative prompt into a fully AI‑generated video."""

    class Input(BlockSchema):
        credentials: CredentialsMetaInput[
            Literal[ProviderName.REVID], Literal["api_key"]
        ] = CredentialsField(description="Revid.ai API credentials")
        prompt: str = SchemaField(
            description="Imaginative prompt describing the desired video.",
            placeholder="A neon‑lit cyberpunk alley with rain‑soaked pavements.",
        )
        ratio: str = SchemaField(default="9 / 16")
        prompt_target_duration: int = SchemaField(default=30)
        voice: Voice = SchemaField(default=Voice.EVA)
        background_music: AudioTrack = SchemaField(
            default=AudioTrack.DONT_STOP_ME_ABSTRACT_FUTURE_BASS
        )

    class Output(BlockSchema):
        video_url: str = SchemaField(description="Rendered video URL")
        error: str = SchemaField(description="Error message if any")

    def __init__(self):
        super().__init__(
            id="46f4099c-ad01-4d79-874c-37a24c937ba3",
            description="Creates an AI video from a single prompt (no line‑breaking script).",
            categories={BlockCategory.AI, BlockCategory.SOCIAL},
            input_schema=AIPromptToVideoCreatorBlock.Input,
            output_schema=AIPromptToVideoCreatorBlock.Output,
            test_input={
                "credentials": TEST_CREDENTIALS_INPUT,
                "prompt": "Epic time‑lapse of a city skyline from day to night",
            },
            test_output=("video_url", "https://example.com/prompt.mp4"),
            test_mock={
                "create_video": lambda api_key, payload: {"pid": "test_pid"},
                "wait_for_video": lambda api_key, pid, max_wait_time=3600: "https://example.com/prompt.mp4",
            },
            test_credentials=TEST_CREDENTIALS,
        )

    def run(self, input_data: Input, *, credentials: APIKeyCredentials, **kwargs):

        payload = {
            "webhook": None,
            "creationParams": {
                "mediaType": "aiVideo",
                "flowType": "prompt-to-video",
                "slug": "prompt-to-video",
                "slugNew": "",
                "isCopiedFrom": False,
                "hasToGenerateVoice": True,
                "hasToTranscript": False,
                "hasToSearchMedia": True,
                "hasAvatar": False,
                "hasWebsiteRecorder": False,
                "hasTextSmallAtBottom": False,
                "ratio": input_data.ratio,
                "selectedAudio": input_data.background_music.value,
                "selectedVoice": input_data.voice.voice_id,
                "selectedAvatar": "https://cdn.revid.ai/avatars/young-woman.mp4",
                "selectedAvatarType": "video/mp4",
                "websiteToRecord": "",
                "hasToGenerateCover": True,
                "nbGenerations": 1,
                "disableCaptions": False,
                "characters": [],
                "captionPresetName": "Revid",
                "sourceType": "contentScraping",
                "selectedStoryStyle": {"value": "custom", "label": "General"},
                "generationPreset": "DEFAULT",
                "hasToGenerateMusic": False,
                "isOptimizedForChinese": False,
                "generationUserPrompt": input_data.prompt,
                "enableNsfwFilter": False,
                "addStickers": False,
                "typeMovingImageAnim": "dynamic",
                "hasToGenerateSoundEffects": False,
                "promptTargetDuration": input_data.prompt_target_duration,
                "selectedCharacters": [],
                "lang": "",
                "voiceSpeed": 1,
                "disableAudio": False,
                "disableVoice": False,
                "imageGenerationModel": "good",
                "videoGenerationModel": "base",
                "hasEnhancedGeneration": True,
                "hasEnhancedGenerationPro": True,
                "inputMedias": [],
                "hasToGenerateVideos": True,
                "audioUrl": input_data.background_music.audio_url,
                "watermark": None,
            },
        }

        response = self.create_video(credentials.api_key, payload)
        pid = response.get("pid")
        if not pid:
            raise RuntimeError("Failed to create video: No project ID returned")

        video_url = self.wait_for_video(credentials.api_key, pid)
        yield "video_url", video_url


class AIScreenshotToVideoAdBlock(Block, _RevidMixin):
    """Creates an advert where the supplied screenshot is narrated by an AI avatar."""

    class Input(BlockSchema):
        credentials: CredentialsMetaInput[
            Literal[ProviderName.REVID], Literal["api_key"]
        ] = CredentialsField(description="Revid.ai API key")
        script: str = SchemaField(
            description="Narration that will accompany the screenshot.",
            placeholder="Check out these amazing stats!",
        )
        screenshot_url: str = SchemaField(
            description="Screenshot or image URL to showcase."
        )
        ratio: str = SchemaField(default="9 / 16")
        target_duration: int = SchemaField(default=30)
        voice: Voice = SchemaField(default=Voice.EVA)
        background_music: AudioTrack = SchemaField(
            default=AudioTrack.DONT_STOP_ME_ABSTRACT_FUTURE_BASS
        )

    class Output(BlockSchema):
        video_url: str = SchemaField(description="Rendered video URL")
        error: str = SchemaField(description="Error, if encountered")

    def __init__(self):
        super().__init__(
            id="9f68982c-3af6-4923-9a97-b50a8c8d2234",
            description="Turns a screenshot into an engaging, avatar‑narrated video advert.",
            categories={BlockCategory.AI, BlockCategory.MARKETING},
            input_schema=AIScreenshotToVideoAdBlock.Input,
            output_schema=AIScreenshotToVideoAdBlock.Output,
            test_input={
                "credentials": TEST_CREDENTIALS_INPUT,
                "script": "Amazing numbers!",
                "screenshot_url": "https://cdn.revid.ai/uploads/1747080376028-image.png",
            },
            test_output=("video_url", "https://example.com/screenshot.mp4"),
            test_mock={
                "create_video": lambda api_key, payload: {"pid": "test_pid"},
                "wait_for_video": lambda api_key, pid, max_wait_time=3600: "https://example.com/screenshot.mp4",
            },
            test_credentials=TEST_CREDENTIALS,
        )

    def run(self, input_data: Input, *, credentials: APIKeyCredentials, **kwargs):

        payload = {
            "webhook": None,
            "creationParams": {
                "targetDuration": input_data.target_duration,
                "ratio": input_data.ratio,
                "mediaType": "aiVideo",
                "hasAvatar": True,
                "removeAvatarBackground": True,
                "inputText": input_data.script,
                "flowType": "text-to-video",
                "slug": "ai-ad-generator",
                "slugNew": "screenshot-to-video-ad",
                "isCopiedFrom": "ai-ad-generator",
                "hasToGenerateVoice": True,
                "hasToTranscript": False,
                "hasToSearchMedia": True,
                "hasWebsiteRecorder": False,
                "hasTextSmallAtBottom": False,
                "selectedAudio": input_data.background_music.value,
                "selectedVoice": input_data.voice.voice_id,
                "selectedAvatar": "https://cdn.revid.ai/avatars/young-woman.mp4",
                "selectedAvatarType": "video/mp4",
                "websiteToRecord": "",
                "hasToGenerateCover": True,
                "nbGenerations": 1,
                "disableCaptions": False,
                "mediaMultiplier": "medium",
                "characters": [],
                "captionPresetName": "Revid",
                "sourceType": "contentScraping",
                "selectedStoryStyle": {"value": "custom", "label": "General"},
                "generationPreset": "DEFAULT",
                "hasToGenerateMusic": False,
                "isOptimizedForChinese": False,
                "generationUserPrompt": "",
                "enableNsfwFilter": False,
                "addStickers": False,
                "typeMovingImageAnim": "dynamic",
                "hasToGenerateSoundEffects": False,
                "forceModelType": "gpt-image-1",
                "selectedCharacters": [],
                "lang": "",
                "voiceSpeed": 1,
                "disableAudio": False,
                "disableVoice": False,
                "useOnlyProvidedMedia": True,
                "imageGenerationModel": "ultra",
                "videoGenerationModel": "base",
                "hasEnhancedGeneration": True,
                "hasEnhancedGenerationPro": True,
                "inputMedias": [
                    {"url": input_data.screenshot_url, "title": "", "type": "image"}
                ],
                "hasToGenerateVideos": True,
                "audioUrl": input_data.background_music.audio_url,
                "watermark": None,
            },
        }

        response = self.create_video(credentials.api_key, payload)
        pid = response.get("pid")
        if not pid:
            raise RuntimeError("Failed to create video: No project ID returned")

        video_url = self.wait_for_video(credentials.api_key, pid)
        yield "video_url", video_url
Redundant Code

Multiple social media posting blocks have nearly identical implementations with only the platform name changed. Consider using a factory pattern or parameterized class to reduce code duplication.

class PostToFacebookBlock(AyrsharePostBlockBase):
    """Block for posting to Facebook."""

    def __init__(self):
        super().__init__(
            id="3352f512-3524-49ed-a08f-003042da2fc1",
            description="Post to Facebook using Ayrshare",
        )

    def run(
        self,
        input_data: AyrsharePostBlockBase.Input,
        *,
        profile_key: SecretStr,
        **kwargs,
    ) -> BlockOutput:
        """Post to Facebook."""
        if not profile_key:
            yield "error", "Please Link a social account via Ayrshare"
            return

        post_result = self._create_post(
            input_data,
            [SocialPlatform.FACEBOOK],
            profile_key=profile_key.get_secret_value(),
        )
        if isinstance(post_result, PostError):
            yield "error", post_result.message
            return
        yield "post_result", post_result


class PostToXBlock(AyrsharePostBlockBase):
    """Block for posting to X / Twitter."""

    def __init__(self):
        super().__init__(
            id="9e8f844e-b4a5-4b25-80f2-9e1dd7d67625",
            description="Post to X / Twitter using Ayrshare",
        )

    def run(
        self,
        input_data: AyrsharePostBlockBase.Input,
        *,
        profile_key: SecretStr,
        **kwargs,
    ) -> BlockOutput:
        """Post to Twitter."""
        if not profile_key:
            yield "error", "Please Link a social account via Ayrshare"
            return

        post_result = self._create_post(
            input_data,
            [SocialPlatform.TWITTER],
            profile_key=profile_key.get_secret_value(),
        )
        if isinstance(post_result, PostError):
            yield "error", post_result.message
            return
        yield "post_result", post_result


class PostToLinkedInBlock(AyrsharePostBlockBase):
    """Block for posting to LinkedIn."""

    def __init__(self):
        super().__init__(
            id="589af4e4-507f-42fd-b9ac-a67ecef25811",
            description="Post to LinkedIn using Ayrshare",
        )

    def run(
        self,
        input_data: AyrsharePostBlockBase.Input,
        *,
        profile_key: SecretStr,
        **kwargs,
    ) -> BlockOutput:
        """Post to LinkedIn."""
        if not profile_key:
            yield "error", "Please Link a social account via Ayrshare"
            return

        post_result = self._create_post(
            input_data,
            [SocialPlatform.LINKEDIN],
            profile_key=profile_key.get_secret_value(),
        )
        if isinstance(post_result, PostError):
            yield "error", post_result.message
            return
        yield "post_result", post_result


class PostToInstagramBlock(AyrsharePostBlockBase):
    """Block for posting to Instagram."""

    def __init__(self):
        super().__init__(
            id="89b02b96-a7cb-46f4-9900-c48b32fe1552",
            description="Post to Instagram using Ayrshare",
        )

    def run(
        self,
        input_data: AyrsharePostBlockBase.Input,
        *,
        profile_key: SecretStr,
        **kwargs,
    ) -> BlockOutput:
        """Post to Instagram."""
        if not profile_key:
            yield "error", "Please Link a social account via Ayrshare"
            return

        post_result = self._create_post(
            input_data,
            [SocialPlatform.INSTAGRAM],
            profile_key=profile_key.get_secret_value(),
        )
        if isinstance(post_result, PostError):
            yield "error", post_result.message
            return
        yield "post_result", post_result


class PostToYouTubeBlock(AyrsharePostBlockBase):
    """Block for posting to YouTube."""

    def __init__(self):
        super().__init__(
            id="0082d712-ff1b-4c3d-8a8d-6c7721883b83",
            description="Post to YouTube using Ayrshare",
        )

    def run(
        self,
        input_data: AyrsharePostBlockBase.Input,
        *,
        profile_key: SecretStr,
        **kwargs,
    ) -> BlockOutput:
        """Post to YouTube."""
        if not profile_key:
            yield "error", "Please Link a social account via Ayrshare"
            return

        post_result = self._create_post(
            input_data,
            [SocialPlatform.YOUTUBE],
            profile_key=profile_key.get_secret_value(),
        )
        if isinstance(post_result, PostError):
            yield "error", post_result.message
            return
        yield "post_result", post_result


class PostToRedditBlock(AyrsharePostBlockBase):
    """Block for posting to Reddit."""

    def __init__(self):
        super().__init__(
            id="c7733580-3c72-483e-8e47-a8d58754d853",
            description="Post to Reddit using Ayrshare",
        )

    def run(
        self,
        input_data: AyrsharePostBlockBase.Input,
        *,
        profile_key: SecretStr,
        **kwargs,
    ) -> BlockOutput:
        """Post to Reddit."""
        if not profile_key:
            yield "error", "Please Link a social account via Ayrshare"
            return

        post_result = self._create_post(
            input_data,
            [SocialPlatform.REDDIT],
            profile_key=profile_key.get_secret_value(),
        )
        if isinstance(post_result, PostError):
            yield "error", post_result.message
            return
        yield "post_result", post_result


class PostToTelegramBlock(AyrsharePostBlockBase):
    """Block for posting to Telegram."""

    def __init__(self):
        super().__init__(
            id="47bc74eb-4af2-452c-b933-af377c7287df",
            description="Post to Telegram using Ayrshare",
        )

    def run(
        self,
        input_data: AyrsharePostBlockBase.Input,
        *,
        profile_key: SecretStr,
        **kwargs,
    ) -> BlockOutput:
        """Post to Telegram."""
        if not profile_key:
            yield "error", "Please Link a social account via Ayrshare"
            return

        post_result = self._create_post(
            input_data,
            [SocialPlatform.TELEGRAM],
            profile_key=profile_key.get_secret_value(),
        )
        if isinstance(post_result, PostError):
            yield "error", post_result.message
            return
        yield "post_result", post_result


class PostToGMBBlock(AyrsharePostBlockBase):
    """Block for posting to Google My Business."""

    def __init__(self):
        super().__init__(
            id="2c38c783-c484-4503-9280-ef5d1d345a7e",
            description="Post to Google My Business using Ayrshare",
        )

    def run(
        self,
        input_data: AyrsharePostBlockBase.Input,
        *,
        profile_key: SecretStr,
        **kwargs,
    ) -> BlockOutput:
        """Post to Google My Business."""
        if not profile_key:
            yield "error", "Please Link a social account via Ayrshare"
            return

        post_result = self._create_post(
            input_data,
            [SocialPlatform.GMB],
            profile_key=profile_key.get_secret_value(),
        )
        if isinstance(post_result, PostError):
            yield "error", post_result.message
            return
        yield "post_result", post_result


class PostToPinterestBlock(AyrsharePostBlockBase):
    """Block for posting to Pinterest."""

    def __init__(self):
        super().__init__(
            id="3ca46e05-dbaa-4afb-9e95-5a429c4177e6",
            description="Post to Pinterest using Ayrshare",
        )

    def run(
        self,
        input_data: AyrsharePostBlockBase.Input,
        *,
        profile_key: SecretStr,
        **kwargs,
    ) -> BlockOutput:
        """Post to Pinterest."""
        if not profile_key:
            yield "error", "Please Link a social account via Ayrshare"
            return

        post_result = self._create_post(
            input_data,
            [SocialPlatform.PINTEREST],
            profile_key=profile_key.get_secret_value(),
        )
        if isinstance(post_result, PostError):
            yield "error", post_result.message
            return
        yield "post_result", post_result


class PostToTikTokBlock(AyrsharePostBlockBase):
    """Block for posting to TikTok."""

    def __init__(self):
        super().__init__(
            id="7faf4b27-96b0-4f05-bf64-e0de54ae74e1",
            description="Post to TikTok using Ayrshare",
        )

    def run(
        self,
        input_data: AyrsharePostBlockBase.Input,
        *,
        profile_key: SecretStr,
        **kwargs,
    ) -> BlockOutput:
        """Post to TikTok."""
        if not profile_key:
            yield "error", "Please Link a social account via Ayrshare"
            return

        post_result = self._create_post(
            input_data,
            [SocialPlatform.TIKTOK],
            profile_key=profile_key.get_secret_value(),
        )
        if isinstance(post_result, PostError):
            yield "error", post_result.message
            return
        yield "post_result", post_result


class PostToBlueskyBlock(AyrsharePostBlockBase):
    """Block for posting to Bluesky."""

    def __init__(self):
        super().__init__(
            id="cbd52c2a-06d2-43ed-9560-6576cc163283",
            description="Post to Bluesky using Ayrshare",
        )

    def run(
        self,
        input_data: AyrsharePostBlockBase.Input,
        *,
        profile_key: SecretStr,
        **kwargs,
    ) -> BlockOutput:
        """Post to Bluesky."""
        if not profile_key:
            yield "error", "Please Link a social account via Ayrshare"
            return

        post_result = self._create_post(
            input_data,
            [SocialPlatform.BLUESKY],
            profile_key=profile_key.get_secret_value(),
        )
        if isinstance(post_result, PostError):
            yield "error", post_result.message
            return
        yield "post_result", post_result
Error Handling

The Ayrshare SSO URL generation endpoint has limited error handling. Consider adding more specific error types and better logging for production troubleshooting.

@router.get("/ayrshare/sso_url")
async def get_ayrshare_sso_url(
    user_id: Annotated[str, Depends(get_user_id)],
) -> dict[str, str]:
    """
    Generate an SSO URL for Ayrshare social media integration.

    Returns:
        dict: Contains the SSO URL for Ayrshare integration
    """
    # Generate JWT and get SSO URL
    client = AyrshareClient()

    # Get or create profile key
    profile_key = creds_manager.store.get_ayrshare_profile_key(user_id)
    if not profile_key:
        logger.info(f"Creating new Ayrshare profile for user {user_id}")
        # Create new profile if none exists
        profile = client.create_profile(title=f"User {user_id}", messaging_active=True)
        if isinstance(profile, PostError):
            logger.error(
                f"Error creating Ayrshare profile for user {user_id}: {profile}"
            )
            raise HTTPException(
                status_code=500, detail="Failed to create Ayrshare profile"
            )
        profile_key = profile.profileKey
        creds_manager.store.set_ayrshare_profile_key(user_id, profile_key)
    else:
        logger.info(f"Using existing Ayrshare profile for user {user_id}")

    # Convert SecretStr to string if needed
    profile_key_str = (
        profile_key.get_secret_value()
        if isinstance(profile_key, SecretStr)
        else str(profile_key)
    )

    private_key = settings.secrets.ayrshare_jwt_key

    try:
        logger.info(f"Generating JWT for user {user_id}")
        jwt_response = client.generate_jwt(
            private_key=private_key,
            profile_key=profile_key_str,
            allowed_social=[
                SocialPlatform.FACEBOOK,
                SocialPlatform.TWITTER,
                SocialPlatform.LINKEDIN,
                SocialPlatform.INSTAGRAM,
                SocialPlatform.YOUTUBE,
                SocialPlatform.REDDIT,
                SocialPlatform.TELEGRAM,
                SocialPlatform.GMB,
                SocialPlatform.PINTEREST,
                SocialPlatform.TIKTOK,
                SocialPlatform.BLUESKY,
            ],
            expires_in=2880,
            verify=True,
        )
    except Exception as e:
        logger.error(f"Error generating JWT for user {user_id}: {e}")
        raise HTTPException(status_code=500, detail="Failed to generate JWT")

    expire_at = datetime.now(timezone.utc) + timedelta(minutes=2880)

Copy link

netlify bot commented May 20, 2025

Deploy Preview for auto-gpt-docs canceled.

Name Link
🔨 Latest commit b0ce552
🔍 Latest deploy log https://app.netlify.com/projects/auto-gpt-docs/deploys/682cb28aa147780008e3e2a7

Copy link

deepsource-io bot commented May 20, 2025

Here's the code health analysis summary for commits 47c1a64..b0ce552. View details on DeepSource ↗.

Analysis Summary

AnalyzerStatusSummaryLink
DeepSource JavaScript LogoJavaScript✅ Success
❗ 45 occurences introduced
🎯 38 occurences resolved
View Check ↗
DeepSource Python LogoPython✅ Success
❗ 18 occurences introduced
🎯 13 occurences resolved
View Check ↗

💡 If you’re a repository administrator, you can configure the quality gates from the settings.

Copy link

codecov bot commented May 20, 2025

Codecov Report

All modified and coverable lines are covered by tests ✅

Project coverage is 34.07%. Comparing base (e5368f3) to head (88360a9).
Report is 9 commits behind head on dev.

✅ All tests successful. No failed tests found.

Additional details and impacted files
@@           Coverage Diff           @@
##              dev    #9988   +/-   ##
=======================================
  Coverage   34.07%   34.07%           
=======================================
  Files          22       22           
  Lines        1893     1893           
  Branches      330      330           
=======================================
  Hits          645      645           
  Misses       1234     1234           
  Partials       14       14           
Flag Coverage Δ
Linux 34.07% <ø> (ø)
Windows 46.04% <ø> (ø)
agbenchmark 34.07% <ø> (ø)
macOS 34.07% <ø> (ø)

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@majdyz majdyz marked this pull request as draft May 20, 2025 13:46
Copy link

netlify bot commented May 20, 2025

Deploy Preview for auto-gpt-docs-dev canceled.

Name Link
🔨 Latest commit b0ce552
🔍 Latest deploy log https://app.netlify.com/projects/auto-gpt-docs-dev/deploys/682cb28ad17c94000861ec64

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
Status: 🆕 Needs initial review
Status: No status
Development

Successfully merging this pull request may close these issues.

5 participants