mirror of
https://github.com/Significant-Gravitas/AutoGPT.git
synced 2026-02-03 19:35:15 -05:00
Compare commits
36 Commits
test/verif
...
aryshare-r
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b0ce5524ce | ||
|
|
52b6d9696b | ||
|
|
a50ef04104 | ||
|
|
fa30bed042 | ||
|
|
88360a9bf2 | ||
|
|
b134bae980 | ||
|
|
694f701194 | ||
|
|
3e1ee56e9f | ||
|
|
ca703fa1f0 | ||
|
|
3c3311c506 | ||
|
|
7e62ce3159 | ||
|
|
9509762a94 | ||
|
|
3eff0a8df2 | ||
|
|
d53da6f572 | ||
|
|
fdf4785ae2 | ||
|
|
f1b5128715 | ||
|
|
d26edd8dac | ||
|
|
15bc5cf228 | ||
|
|
2d3842bdd1 | ||
|
|
e7dde98cf8 | ||
|
|
cae4a6e145 | ||
|
|
e71a422173 | ||
|
|
6912e3ade3 | ||
|
|
7d90376eb2 | ||
|
|
993e123f1b | ||
|
|
863a9e98ec | ||
|
|
7f996745ce | ||
|
|
e93ead51a9 | ||
|
|
b7a6dfdb19 | ||
|
|
2ea099c265 | ||
|
|
bf70991e10 | ||
|
|
b1e6161a7d | ||
|
|
957706a941 | ||
|
|
2b9f092b5c | ||
|
|
2afdd4d207 | ||
|
|
d7af738358 |
@@ -197,6 +197,10 @@ SMARTLEAD_API_KEY=
|
|||||||
# ZeroBounce
|
# ZeroBounce
|
||||||
ZEROBOUNCE_API_KEY=
|
ZEROBOUNCE_API_KEY=
|
||||||
|
|
||||||
|
# Ayrshare
|
||||||
|
AYRSHARE_API_KEY=
|
||||||
|
AYRSHARE_JWT_KEY=
|
||||||
|
|
||||||
## ===== OPTIONAL API KEYS END ===== ##
|
## ===== OPTIONAL API KEYS END ===== ##
|
||||||
|
|
||||||
# Logging Configuration
|
# Logging Configuration
|
||||||
|
|||||||
@@ -52,6 +52,7 @@ class AudioTrack(str, Enum):
|
|||||||
REFRESHER = ("Refresher",)
|
REFRESHER = ("Refresher",)
|
||||||
TOURIST = ("Tourist",)
|
TOURIST = ("Tourist",)
|
||||||
TWIN_TYCHES = ("Twin Tyches",)
|
TWIN_TYCHES = ("Twin Tyches",)
|
||||||
|
DONT_STOP_ME_ABSTRACT_FUTURE_BASS = ("Dont Stop Me Abstract Future Bass",)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def audio_url(self):
|
def audio_url(self):
|
||||||
@@ -77,6 +78,7 @@ class AudioTrack(str, Enum):
|
|||||||
AudioTrack.REFRESHER: "https://cdn.tfrv.xyz/audio/refresher.mp3",
|
AudioTrack.REFRESHER: "https://cdn.tfrv.xyz/audio/refresher.mp3",
|
||||||
AudioTrack.TOURIST: "https://cdn.tfrv.xyz/audio/tourist.mp3",
|
AudioTrack.TOURIST: "https://cdn.tfrv.xyz/audio/tourist.mp3",
|
||||||
AudioTrack.TWIN_TYCHES: "https://cdn.tfrv.xyz/audio/twin-tynches.mp3",
|
AudioTrack.TWIN_TYCHES: "https://cdn.tfrv.xyz/audio/twin-tynches.mp3",
|
||||||
|
AudioTrack.DONT_STOP_ME_ABSTRACT_FUTURE_BASS: "https://cdn.revid.ai/audio/_dont-stop-me-abstract-future-bass.mp3",
|
||||||
}
|
}
|
||||||
return audio_urls[self]
|
return audio_urls[self]
|
||||||
|
|
||||||
@@ -104,6 +106,7 @@ class GenerationPreset(str, Enum):
|
|||||||
MOVIE = ("Movie",)
|
MOVIE = ("Movie",)
|
||||||
STYLIZED_ILLUSTRATION = ("Stylized Illustration",)
|
STYLIZED_ILLUSTRATION = ("Stylized Illustration",)
|
||||||
MANGA = ("Manga",)
|
MANGA = ("Manga",)
|
||||||
|
DEFAULT = ("DEFAULT",)
|
||||||
|
|
||||||
|
|
||||||
class Voice(str, Enum):
|
class Voice(str, Enum):
|
||||||
@@ -113,6 +116,7 @@ class Voice(str, Enum):
|
|||||||
JESSICA = "Jessica"
|
JESSICA = "Jessica"
|
||||||
CHARLOTTE = "Charlotte"
|
CHARLOTTE = "Charlotte"
|
||||||
CALLUM = "Callum"
|
CALLUM = "Callum"
|
||||||
|
EVA = "Eva"
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def voice_id(self):
|
def voice_id(self):
|
||||||
@@ -123,6 +127,7 @@ class Voice(str, Enum):
|
|||||||
Voice.JESSICA: "cgSgspJ2msm6clMCkdW9",
|
Voice.JESSICA: "cgSgspJ2msm6clMCkdW9",
|
||||||
Voice.CHARLOTTE: "XB0fDUnXU5powFXDhCwa",
|
Voice.CHARLOTTE: "XB0fDUnXU5powFXDhCwa",
|
||||||
Voice.CALLUM: "N2lVS1w4EtoT3dr4eOWO",
|
Voice.CALLUM: "N2lVS1w4EtoT3dr4eOWO",
|
||||||
|
Voice.EVA: "FGY2WhTYpPnrIDTdsKH5",
|
||||||
}
|
}
|
||||||
return voice_id_map[self]
|
return voice_id_map[self]
|
||||||
|
|
||||||
@@ -139,7 +144,54 @@ class VisualMediaType(str, Enum):
|
|||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
class AIShortformVideoCreatorBlock(Block):
|
class _RevidMixin:
|
||||||
|
"""Utility mix‑in that bundles the shared webhook / polling helpers."""
|
||||||
|
|
||||||
|
def create_video(self, api_key: SecretStr, payload: dict) -> dict:
|
||||||
|
url = "https://www.revid.ai/api/public/v2/render"
|
||||||
|
headers = {"key": api_key.get_secret_value()}
|
||||||
|
response = requests.post(url, json=payload, headers=headers)
|
||||||
|
logger.debug(
|
||||||
|
f"API Response Status Code: {response.status_code}, Content: {response.text}"
|
||||||
|
)
|
||||||
|
return response.json()
|
||||||
|
|
||||||
|
def check_video_status(self, api_key: SecretStr, pid: str) -> dict:
|
||||||
|
url = f"https://www.revid.ai/api/public/v2/status?pid={pid}"
|
||||||
|
headers = {"key": api_key.get_secret_value()}
|
||||||
|
response = requests.get(url, headers=headers)
|
||||||
|
return response.json()
|
||||||
|
|
||||||
|
def wait_for_video(
|
||||||
|
self,
|
||||||
|
api_key: SecretStr,
|
||||||
|
pid: str,
|
||||||
|
max_wait_time: int = 3600,
|
||||||
|
) -> str:
|
||||||
|
start_time = time.time()
|
||||||
|
while time.time() - start_time < max_wait_time:
|
||||||
|
status = self.check_video_status(api_key, pid)
|
||||||
|
logger.debug(f"Video status: {status}")
|
||||||
|
|
||||||
|
if status.get("status") == "ready" and "videoUrl" in status:
|
||||||
|
return status["videoUrl"]
|
||||||
|
elif status.get("status") == "error":
|
||||||
|
error_message = status.get("error", "Unknown error occurred")
|
||||||
|
logger.error(f"Video creation failed: {error_message}")
|
||||||
|
raise ValueError(f"Video creation failed: {error_message}")
|
||||||
|
elif status.get("status") in ["FAILED", "CANCELED"]:
|
||||||
|
logger.error(f"Video creation failed: {status.get('message')}")
|
||||||
|
raise ValueError(f"Video creation failed: {status.get('message')}")
|
||||||
|
|
||||||
|
time.sleep(10)
|
||||||
|
|
||||||
|
logger.error("Video creation timed out")
|
||||||
|
raise TimeoutError("Video creation timed out")
|
||||||
|
|
||||||
|
|
||||||
|
class AIShortformVideoCreatorBlock(Block, _RevidMixin):
|
||||||
|
"""Creates a short‑form text‑to‑video clip using stock or AI imagery."""
|
||||||
|
|
||||||
class Input(BlockSchema):
|
class Input(BlockSchema):
|
||||||
credentials: CredentialsMetaInput[
|
credentials: CredentialsMetaInput[
|
||||||
Literal[ProviderName.REVID], Literal["api_key"]
|
Literal[ProviderName.REVID], Literal["api_key"]
|
||||||
@@ -206,86 +258,28 @@ class AIShortformVideoCreatorBlock(Block):
|
|||||||
"https://example.com/video.mp4",
|
"https://example.com/video.mp4",
|
||||||
),
|
),
|
||||||
test_mock={
|
test_mock={
|
||||||
"create_webhook": lambda: (
|
|
||||||
"test_uuid",
|
|
||||||
"https://webhook.site/test_uuid",
|
|
||||||
),
|
|
||||||
"create_video": lambda api_key, payload: {"pid": "test_pid"},
|
"create_video": lambda api_key, payload: {"pid": "test_pid"},
|
||||||
"wait_for_video": lambda api_key, pid, webhook_token, max_wait_time=1000: "https://example.com/video.mp4",
|
"wait_for_video": lambda api_key, pid, max_wait_time=3600: "https://example.com/video.mp4",
|
||||||
},
|
},
|
||||||
test_credentials=TEST_CREDENTIALS,
|
test_credentials=TEST_CREDENTIALS,
|
||||||
)
|
)
|
||||||
|
|
||||||
def create_webhook(self):
|
|
||||||
url = "https://webhook.site/token"
|
|
||||||
headers = {"Accept": "application/json", "Content-Type": "application/json"}
|
|
||||||
response = requests.post(url, headers=headers)
|
|
||||||
webhook_data = response.json()
|
|
||||||
return webhook_data["uuid"], f"https://webhook.site/{webhook_data['uuid']}"
|
|
||||||
|
|
||||||
def create_video(self, api_key: SecretStr, payload: dict) -> dict:
|
|
||||||
url = "https://www.revid.ai/api/public/v2/render"
|
|
||||||
headers = {"key": api_key.get_secret_value()}
|
|
||||||
response = requests.post(url, json=payload, headers=headers)
|
|
||||||
logger.debug(
|
|
||||||
f"API Response Status Code: {response.status_code}, Content: {response.text}"
|
|
||||||
)
|
|
||||||
return response.json()
|
|
||||||
|
|
||||||
def check_video_status(self, api_key: SecretStr, pid: str) -> dict:
|
|
||||||
url = f"https://www.revid.ai/api/public/v2/status?pid={pid}"
|
|
||||||
headers = {"key": api_key.get_secret_value()}
|
|
||||||
response = requests.get(url, headers=headers)
|
|
||||||
return response.json()
|
|
||||||
|
|
||||||
def wait_for_video(
|
|
||||||
self,
|
|
||||||
api_key: SecretStr,
|
|
||||||
pid: str,
|
|
||||||
webhook_token: str,
|
|
||||||
max_wait_time: int = 1000,
|
|
||||||
) -> str:
|
|
||||||
start_time = time.time()
|
|
||||||
while time.time() - start_time < max_wait_time:
|
|
||||||
status = self.check_video_status(api_key, pid)
|
|
||||||
logger.debug(f"Video status: {status}")
|
|
||||||
|
|
||||||
if status.get("status") == "ready" and "videoUrl" in status:
|
|
||||||
return status["videoUrl"]
|
|
||||||
elif status.get("status") == "error":
|
|
||||||
error_message = status.get("error", "Unknown error occurred")
|
|
||||||
logger.error(f"Video creation failed: {error_message}")
|
|
||||||
raise ValueError(f"Video creation failed: {error_message}")
|
|
||||||
elif status.get("status") in ["FAILED", "CANCELED"]:
|
|
||||||
logger.error(f"Video creation failed: {status.get('message')}")
|
|
||||||
raise ValueError(f"Video creation failed: {status.get('message')}")
|
|
||||||
|
|
||||||
time.sleep(10)
|
|
||||||
|
|
||||||
logger.error("Video creation timed out")
|
|
||||||
raise TimeoutError("Video creation timed out")
|
|
||||||
|
|
||||||
def run(
|
def run(
|
||||||
self, input_data: Input, *, credentials: APIKeyCredentials, **kwargs
|
self, input_data: Input, *, credentials: APIKeyCredentials, **kwargs
|
||||||
) -> BlockOutput:
|
) -> BlockOutput:
|
||||||
# Create a new Webhook.site URL
|
|
||||||
webhook_token, webhook_url = self.create_webhook()
|
|
||||||
logger.debug(f"Webhook URL: {webhook_url}")
|
|
||||||
|
|
||||||
audio_url = input_data.background_music.audio_url
|
|
||||||
|
|
||||||
payload = {
|
payload = {
|
||||||
"frameRate": input_data.frame_rate,
|
"frameRate": input_data.frame_rate,
|
||||||
"resolution": input_data.resolution,
|
"resolution": input_data.resolution,
|
||||||
"frameDurationMultiplier": 18,
|
"frameDurationMultiplier": 18,
|
||||||
"webhook": webhook_url,
|
"webhook": None,
|
||||||
"creationParams": {
|
"creationParams": {
|
||||||
"mediaType": input_data.video_style,
|
"mediaType": input_data.video_style,
|
||||||
"captionPresetName": "Wrap 1",
|
"captionPresetName": "Wrap 1",
|
||||||
"selectedVoice": input_data.voice.voice_id,
|
"selectedVoice": input_data.voice.voice_id,
|
||||||
"hasEnhancedGeneration": True,
|
"hasEnhancedGeneration": True,
|
||||||
"generationPreset": input_data.generation_preset.name,
|
"generationPreset": input_data.generation_preset.name,
|
||||||
"selectedAudio": input_data.background_music,
|
"selectedAudio": input_data.background_music.value,
|
||||||
"origin": "/create",
|
"origin": "/create",
|
||||||
"inputText": input_data.script,
|
"inputText": input_data.script,
|
||||||
"flowType": "text-to-video",
|
"flowType": "text-to-video",
|
||||||
@@ -301,7 +295,7 @@ class AIShortformVideoCreatorBlock(Block):
|
|||||||
"selectedStoryStyle": {"value": "custom", "label": "Custom"},
|
"selectedStoryStyle": {"value": "custom", "label": "Custom"},
|
||||||
"hasToGenerateVideos": input_data.video_style
|
"hasToGenerateVideos": input_data.video_style
|
||||||
!= VisualMediaType.STOCK_VIDEOS,
|
!= VisualMediaType.STOCK_VIDEOS,
|
||||||
"audioUrl": audio_url,
|
"audioUrl": input_data.background_music.audio_url,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -314,10 +308,354 @@ class AIShortformVideoCreatorBlock(Block):
|
|||||||
f"Failed to create video: No project ID returned. API Response: {response}"
|
f"Failed to create video: No project ID returned. API Response: {response}"
|
||||||
)
|
)
|
||||||
raise RuntimeError("Failed to create video: No project ID returned")
|
raise RuntimeError("Failed to create video: No project ID returned")
|
||||||
else:
|
|
||||||
logger.debug(
|
logger.debug(f"Video created with project ID: {pid}. Waiting for completion...")
|
||||||
f"Video created with project ID: {pid}. Waiting for completion..."
|
video_url = self.wait_for_video(credentials.api_key, pid)
|
||||||
)
|
logger.debug(f"Video ready: {video_url}")
|
||||||
video_url = self.wait_for_video(credentials.api_key, pid, webhook_token)
|
yield "video_url", video_url
|
||||||
logger.debug(f"Video ready: {video_url}")
|
|
||||||
yield "video_url", video_url
|
|
||||||
|
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
|
||||||
|
|||||||
482
autogpt_platform/backend/backend/blocks/ayrshare/_api.py
Normal file
482
autogpt_platform/backend/backend/blocks/ayrshare/_api.py
Normal file
@@ -0,0 +1,482 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import json
|
||||||
|
import logging
|
||||||
|
from dataclasses import dataclass
|
||||||
|
from enum import Enum
|
||||||
|
from typing import Any, Dict, List, Optional, Union
|
||||||
|
|
||||||
|
from backend.util.request import Requests
|
||||||
|
from backend.util.settings import Settings
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
settings = Settings()
|
||||||
|
|
||||||
|
|
||||||
|
class AyrshareAPIException(Exception):
|
||||||
|
def __init__(self, message: str, status_code: int):
|
||||||
|
super().__init__(message)
|
||||||
|
self.status_code = status_code
|
||||||
|
|
||||||
|
|
||||||
|
class SocialPlatform(str, Enum):
|
||||||
|
BLUESKY = "bluesky"
|
||||||
|
FACEBOOK = "facebook"
|
||||||
|
TWITTER = "twitter"
|
||||||
|
LINKEDIN = "linkedin"
|
||||||
|
INSTAGRAM = "instagram"
|
||||||
|
YOUTUBE = "youtube"
|
||||||
|
REDDIT = "reddit"
|
||||||
|
TELEGRAM = "telegram"
|
||||||
|
GMB = "gmb"
|
||||||
|
PINTEREST = "pinterest"
|
||||||
|
TIKTOK = "tiktok"
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class EmailConfig:
|
||||||
|
to: str
|
||||||
|
subject: Optional[str] = None
|
||||||
|
body: Optional[str] = None
|
||||||
|
from_name: Optional[str] = None
|
||||||
|
from_email: Optional[str] = None
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class JWTResponse:
|
||||||
|
status: str
|
||||||
|
title: str
|
||||||
|
token: str
|
||||||
|
url: str
|
||||||
|
emailSent: Optional[bool] = None
|
||||||
|
expiresIn: Optional[str] = None
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class ProfileResponse:
|
||||||
|
status: str
|
||||||
|
title: str
|
||||||
|
refId: str
|
||||||
|
profileKey: str
|
||||||
|
messagingActive: Optional[bool] = None
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class PostResponse:
|
||||||
|
status: str
|
||||||
|
id: str
|
||||||
|
refId: str
|
||||||
|
profileTitle: str
|
||||||
|
post: str
|
||||||
|
postIds: Optional[List[Dict[str, Any]]] = None
|
||||||
|
scheduleDate: Optional[str] = None
|
||||||
|
errors: Optional[List[str]] = None
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class AutoHashtag:
|
||||||
|
max: Optional[int] = None
|
||||||
|
position: Optional[str] = None
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class FirstComment:
|
||||||
|
text: str
|
||||||
|
platforms: Optional[List[SocialPlatform]] = None
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class AutoSchedule:
|
||||||
|
interval: str
|
||||||
|
platforms: Optional[List[SocialPlatform]] = None
|
||||||
|
startDate: Optional[str] = None
|
||||||
|
endDate: Optional[str] = None
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class AutoRepost:
|
||||||
|
interval: str
|
||||||
|
platforms: Optional[List[SocialPlatform]] = None
|
||||||
|
startDate: Optional[str] = None
|
||||||
|
endDate: Optional[str] = None
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class PostError:
|
||||||
|
code: int
|
||||||
|
message: str
|
||||||
|
details: str
|
||||||
|
|
||||||
|
|
||||||
|
class AyrshareClient:
|
||||||
|
"""Client for the Ayrshare Social Media Post API"""
|
||||||
|
|
||||||
|
API_URL = "https://api.ayrshare.com/api"
|
||||||
|
POST_ENDPOINT = f"{API_URL}/post"
|
||||||
|
PROFILES_ENDPOINT = f"{API_URL}/profiles"
|
||||||
|
JWT_ENDPOINT = f"{PROFILES_ENDPOINT}/generateJWT"
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
custom_requests: Optional[Requests] = None,
|
||||||
|
):
|
||||||
|
headers: Dict[str, str] = {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
"Authorization": f"Bearer {settings.secrets.ayrshare_api_key}",
|
||||||
|
}
|
||||||
|
self.headers = headers
|
||||||
|
|
||||||
|
if custom_requests:
|
||||||
|
self._requests = custom_requests
|
||||||
|
else:
|
||||||
|
self._requests = Requests(
|
||||||
|
extra_headers=headers,
|
||||||
|
trusted_origins=["https://api.ayrshare.com"],
|
||||||
|
raise_for_status=False,
|
||||||
|
)
|
||||||
|
|
||||||
|
def generate_jwt(
|
||||||
|
self,
|
||||||
|
private_key: str,
|
||||||
|
profile_key: str,
|
||||||
|
logout: Optional[bool] = None,
|
||||||
|
redirect: Optional[str] = None,
|
||||||
|
allowed_social: Optional[List[SocialPlatform]] = None,
|
||||||
|
verify: Optional[bool] = None,
|
||||||
|
base64: Optional[bool] = None,
|
||||||
|
expires_in: Optional[int] = None,
|
||||||
|
email: Optional[EmailConfig] = None,
|
||||||
|
) -> JWTResponse:
|
||||||
|
"""
|
||||||
|
Generate a JSON Web Token (JWT) for use with single sign on.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
domain: Domain of app. Must match the domain given during onboarding.
|
||||||
|
private_key: Private Key used for encryption.
|
||||||
|
profile_key: User Profile Key (not the API Key).
|
||||||
|
logout: Automatically logout the current session.
|
||||||
|
redirect: URL to redirect to when the "Done" button or logo is clicked.
|
||||||
|
allowed_social: List of social networks to display in the linking page.
|
||||||
|
verify: Verify that the generated token is valid (recommended for non-production).
|
||||||
|
base64: Whether the private key is base64 encoded.
|
||||||
|
expires_in: Token longevity in minutes (1-2880).
|
||||||
|
email: Configuration for sending Connect Accounts email.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
JWTResponse object containing the JWT token and URL.
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
AyrshareAPIException: If the API request fails or private key is invalid.
|
||||||
|
"""
|
||||||
|
payload: Dict[str, Any] = {
|
||||||
|
"domain": "id-pojeg",
|
||||||
|
"privateKey": private_key,
|
||||||
|
"profileKey": profile_key,
|
||||||
|
}
|
||||||
|
|
||||||
|
headers = self.headers
|
||||||
|
headers["Profile-Key"] = profile_key
|
||||||
|
if logout is not None:
|
||||||
|
payload["logout"] = logout
|
||||||
|
if redirect is not None:
|
||||||
|
payload["redirect"] = redirect
|
||||||
|
if allowed_social is not None:
|
||||||
|
payload["allowedSocial"] = [p.value for p in allowed_social]
|
||||||
|
if verify is not None:
|
||||||
|
payload["verify"] = verify
|
||||||
|
if base64 is not None:
|
||||||
|
payload["base64"] = base64
|
||||||
|
if expires_in is not None:
|
||||||
|
payload["expiresIn"] = expires_in
|
||||||
|
if email is not None:
|
||||||
|
payload["email"] = email.__dict__
|
||||||
|
|
||||||
|
response = self._requests.post(self.JWT_ENDPOINT, json=payload, headers=headers)
|
||||||
|
|
||||||
|
if not response.ok:
|
||||||
|
try:
|
||||||
|
error_data = response.json()
|
||||||
|
error_message = error_data.get("message", response.text)
|
||||||
|
except json.JSONDecodeError:
|
||||||
|
error_message = response.text
|
||||||
|
|
||||||
|
raise AyrshareAPIException(
|
||||||
|
f"Ayrshare API request failed ({response.status_code}): {error_message}",
|
||||||
|
response.status_code,
|
||||||
|
)
|
||||||
|
|
||||||
|
response_data = response.json()
|
||||||
|
if response_data.get("status") != "success":
|
||||||
|
raise AyrshareAPIException(
|
||||||
|
f"Ayrshare API returned error: {response_data.get('message', 'Unknown error')}",
|
||||||
|
response.status_code,
|
||||||
|
)
|
||||||
|
|
||||||
|
return JWTResponse(**response_data)
|
||||||
|
|
||||||
|
def create_profile(
|
||||||
|
self,
|
||||||
|
title: str,
|
||||||
|
messaging_active: Optional[bool] = None,
|
||||||
|
hide_top_header: Optional[bool] = None,
|
||||||
|
top_header: Optional[str] = None,
|
||||||
|
disable_social: Optional[List[SocialPlatform]] = None,
|
||||||
|
team: Optional[bool] = None,
|
||||||
|
email: Optional[str] = None,
|
||||||
|
sub_header: Optional[str] = None,
|
||||||
|
tags: Optional[List[str]] = None,
|
||||||
|
) -> ProfileResponse | PostError:
|
||||||
|
"""
|
||||||
|
Create a new User Profile under your Primary Profile.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
title: Title of the new profile. Must be unique.
|
||||||
|
messaging_active: Set to true to activate messaging for this user profile.
|
||||||
|
hide_top_header: Hide the top header on the social accounts linkage page.
|
||||||
|
top_header: Change the header on the social accounts linkage page.
|
||||||
|
disable_social: Array of social networks that are disabled for this user's profile.
|
||||||
|
team: Create a new user profile as a team member.
|
||||||
|
email: Email address for team member invite (required if team is true).
|
||||||
|
sub_header: Change the sub header on the social accounts linkage page.
|
||||||
|
tags: Array of strings to tag user profiles.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
ProfileResponse object containing the profile details and profile key.
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
AyrshareAPIException: If the API request fails or profile title already exists.
|
||||||
|
"""
|
||||||
|
payload: Dict[str, Any] = {
|
||||||
|
"title": title,
|
||||||
|
}
|
||||||
|
|
||||||
|
if messaging_active is not None:
|
||||||
|
payload["messagingActive"] = messaging_active
|
||||||
|
if hide_top_header is not None:
|
||||||
|
payload["hideTopHeader"] = hide_top_header
|
||||||
|
if top_header is not None:
|
||||||
|
payload["topHeader"] = top_header
|
||||||
|
if disable_social is not None:
|
||||||
|
payload["disableSocial"] = [p.value for p in disable_social]
|
||||||
|
if team is not None:
|
||||||
|
payload["team"] = team
|
||||||
|
if email is not None:
|
||||||
|
payload["email"] = email
|
||||||
|
if sub_header is not None:
|
||||||
|
payload["subHeader"] = sub_header
|
||||||
|
if tags is not None:
|
||||||
|
payload["tags"] = tags
|
||||||
|
|
||||||
|
response = self._requests.post(self.PROFILES_ENDPOINT, json=payload)
|
||||||
|
|
||||||
|
if not response.ok:
|
||||||
|
try:
|
||||||
|
error_data = response.json()
|
||||||
|
error_message = error_data.get("message", response.text)
|
||||||
|
except json.JSONDecodeError:
|
||||||
|
error_message = response.text
|
||||||
|
|
||||||
|
raise AyrshareAPIException(
|
||||||
|
f"Ayrshare API request failed ({response.status_code}): {error_message}",
|
||||||
|
response.status_code,
|
||||||
|
)
|
||||||
|
|
||||||
|
response_data = response.json()
|
||||||
|
if response_data.get("status") != "success":
|
||||||
|
raise AyrshareAPIException(
|
||||||
|
f"Ayrshare API returned error: {response_data.get('message', 'Unknown error')}",
|
||||||
|
response.status_code,
|
||||||
|
)
|
||||||
|
|
||||||
|
return ProfileResponse(**response_data)
|
||||||
|
|
||||||
|
def create_post(
|
||||||
|
self,
|
||||||
|
post: str,
|
||||||
|
platforms: List[SocialPlatform],
|
||||||
|
media_urls: Optional[List[str]] = None,
|
||||||
|
is_video: Optional[bool] = None,
|
||||||
|
schedule_date: Optional[str] = None,
|
||||||
|
first_comment: Optional[FirstComment] = None,
|
||||||
|
disable_comments: Optional[bool] = None,
|
||||||
|
shorten_links: Optional[bool] = None,
|
||||||
|
auto_schedule: Optional[AutoSchedule] = None,
|
||||||
|
auto_repost: Optional[AutoRepost] = None,
|
||||||
|
auto_hashtag: Optional[Union[AutoHashtag, bool]] = None,
|
||||||
|
unsplash: Optional[str] = None,
|
||||||
|
bluesky_options: Optional[Dict[str, Any]] = None,
|
||||||
|
facebook_options: Optional[Dict[str, Any]] = None,
|
||||||
|
gmb_options: Optional[Dict[str, Any]] = None,
|
||||||
|
instagram_options: Optional[Dict[str, Any]] = None,
|
||||||
|
linkedin_options: Optional[Dict[str, Any]] = None,
|
||||||
|
pinterest_options: Optional[Dict[str, Any]] = None,
|
||||||
|
reddit_options: Optional[Dict[str, Any]] = None,
|
||||||
|
telegram_options: Optional[Dict[str, Any]] = None,
|
||||||
|
threads_options: Optional[Dict[str, Any]] = None,
|
||||||
|
tiktok_options: Optional[Dict[str, Any]] = None,
|
||||||
|
twitter_options: Optional[Dict[str, Any]] = None,
|
||||||
|
youtube_options: Optional[Dict[str, Any]] = None,
|
||||||
|
requires_approval: Optional[bool] = None,
|
||||||
|
random_post: Optional[bool] = None,
|
||||||
|
random_media_url: Optional[bool] = None,
|
||||||
|
idempotency_key: Optional[str] = None,
|
||||||
|
notes: Optional[str] = None,
|
||||||
|
profile_key: Optional[str] = None,
|
||||||
|
) -> PostResponse | PostError:
|
||||||
|
"""
|
||||||
|
Create a post across multiple social media platforms.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
post: The post text to be published
|
||||||
|
platforms: List of platforms to post to (e.g. [SocialPlatform.TWITTER, SocialPlatform.FACEBOOK])
|
||||||
|
media_urls: Optional list of media URLs to include
|
||||||
|
is_video: Whether the media is a video
|
||||||
|
schedule_date: UTC datetime for scheduling (YYYY-MM-DDThh:mm:ssZ)
|
||||||
|
first_comment: Configuration for first comment
|
||||||
|
disable_comments: Whether to disable comments
|
||||||
|
shorten_links: Whether to shorten links
|
||||||
|
auto_schedule: Configuration for automatic scheduling
|
||||||
|
auto_repost: Configuration for automatic reposting
|
||||||
|
auto_hashtag: Configuration for automatic hashtags
|
||||||
|
unsplash: Unsplash image configuration
|
||||||
|
bluesky_options: Bluesky-specific options
|
||||||
|
facebook_options: Facebook-specific options
|
||||||
|
gmb_options: Google Business Profile options
|
||||||
|
instagram_options: Instagram-specific options
|
||||||
|
linkedin_options: LinkedIn-specific options
|
||||||
|
pinterest_options: Pinterest-specific options
|
||||||
|
reddit_options: Reddit-specific options
|
||||||
|
telegram_options: Telegram-specific options
|
||||||
|
threads_options: Threads-specific options
|
||||||
|
tiktok_options: TikTok-specific options
|
||||||
|
twitter_options: Twitter-specific options
|
||||||
|
youtube_options: YouTube-specific options
|
||||||
|
requires_approval: Whether to enable approval workflow
|
||||||
|
random_post: Whether to generate random post text
|
||||||
|
random_media_url: Whether to generate random media
|
||||||
|
idempotency_key: Unique ID for the post
|
||||||
|
notes: Additional notes for the post
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
PostResponse object containing the post details and status
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
AyrshareAPIException: If the API request fails
|
||||||
|
"""
|
||||||
|
|
||||||
|
payload: Dict[str, Any] = {
|
||||||
|
"post": post,
|
||||||
|
"platforms": [p.value for p in platforms],
|
||||||
|
}
|
||||||
|
|
||||||
|
# Add optional parameters if provided
|
||||||
|
if media_urls:
|
||||||
|
payload["mediaUrls"] = media_urls
|
||||||
|
if is_video is not None:
|
||||||
|
payload["isVideo"] = is_video
|
||||||
|
if schedule_date:
|
||||||
|
payload["scheduleDate"] = schedule_date
|
||||||
|
if first_comment:
|
||||||
|
first_comment_dict = first_comment.__dict__.copy()
|
||||||
|
if first_comment.platforms:
|
||||||
|
first_comment_dict["platforms"] = [
|
||||||
|
p.value for p in first_comment.platforms
|
||||||
|
]
|
||||||
|
payload["firstComment"] = first_comment_dict
|
||||||
|
if disable_comments is not None:
|
||||||
|
payload["disableComments"] = disable_comments
|
||||||
|
if shorten_links is not None:
|
||||||
|
payload["shortenLinks"] = shorten_links
|
||||||
|
if auto_schedule:
|
||||||
|
auto_schedule_dict = auto_schedule.__dict__.copy()
|
||||||
|
if auto_schedule.platforms:
|
||||||
|
auto_schedule_dict["platforms"] = [
|
||||||
|
p.value for p in auto_schedule.platforms
|
||||||
|
]
|
||||||
|
payload["autoSchedule"] = auto_schedule_dict
|
||||||
|
if auto_repost:
|
||||||
|
auto_repost_dict = auto_repost.__dict__.copy()
|
||||||
|
if auto_repost.platforms:
|
||||||
|
auto_repost_dict["platforms"] = [p.value for p in auto_repost.platforms]
|
||||||
|
payload["autoRepost"] = auto_repost_dict
|
||||||
|
if auto_hashtag:
|
||||||
|
payload["autoHashtag"] = (
|
||||||
|
auto_hashtag.__dict__
|
||||||
|
if isinstance(auto_hashtag, AutoHashtag)
|
||||||
|
else auto_hashtag
|
||||||
|
)
|
||||||
|
if unsplash:
|
||||||
|
payload["unsplash"] = unsplash
|
||||||
|
if bluesky_options:
|
||||||
|
payload["blueskyOptions"] = bluesky_options
|
||||||
|
if facebook_options:
|
||||||
|
payload["faceBookOptions"] = facebook_options
|
||||||
|
if gmb_options:
|
||||||
|
payload["gmbOptions"] = gmb_options
|
||||||
|
if instagram_options:
|
||||||
|
payload["instagramOptions"] = instagram_options
|
||||||
|
if linkedin_options:
|
||||||
|
payload["linkedInOptions"] = linkedin_options
|
||||||
|
if pinterest_options:
|
||||||
|
payload["pinterestOptions"] = pinterest_options
|
||||||
|
if reddit_options:
|
||||||
|
payload["redditOptions"] = reddit_options
|
||||||
|
if telegram_options:
|
||||||
|
payload["telegramOptions"] = telegram_options
|
||||||
|
if threads_options:
|
||||||
|
payload["threadsOptions"] = threads_options
|
||||||
|
if tiktok_options:
|
||||||
|
payload["tikTokOptions"] = tiktok_options
|
||||||
|
if twitter_options:
|
||||||
|
payload["twitterOptions"] = twitter_options
|
||||||
|
if youtube_options:
|
||||||
|
payload["youTubeOptions"] = youtube_options
|
||||||
|
if requires_approval is not None:
|
||||||
|
payload["requiresApproval"] = requires_approval
|
||||||
|
if random_post is not None:
|
||||||
|
payload["randomPost"] = random_post
|
||||||
|
if random_media_url is not None:
|
||||||
|
payload["randomMediaUrl"] = random_media_url
|
||||||
|
if idempotency_key:
|
||||||
|
payload["idempotencyKey"] = idempotency_key
|
||||||
|
if notes:
|
||||||
|
payload["notes"] = notes
|
||||||
|
|
||||||
|
headers = self.headers
|
||||||
|
if profile_key:
|
||||||
|
headers["Profile-Key"] = profile_key
|
||||||
|
|
||||||
|
response = self._requests.post(
|
||||||
|
self.POST_ENDPOINT, json=payload, headers=headers
|
||||||
|
)
|
||||||
|
|
||||||
|
if not response.ok:
|
||||||
|
try:
|
||||||
|
error_data = response.json()
|
||||||
|
error_message = error_data.get("message", response.text)
|
||||||
|
error_code = error_data.get("code", response.status_code)
|
||||||
|
error_details = error_data.get("details", {})
|
||||||
|
logger.error(error_data)
|
||||||
|
return PostError(
|
||||||
|
code=error_code,
|
||||||
|
message=error_message,
|
||||||
|
details=error_details,
|
||||||
|
)
|
||||||
|
except json.JSONDecodeError:
|
||||||
|
error_message = response.text
|
||||||
|
|
||||||
|
raise AyrshareAPIException(
|
||||||
|
f"Ayrshare API request failed ({response.status_code}): {error_message}",
|
||||||
|
response.status_code,
|
||||||
|
)
|
||||||
|
|
||||||
|
response_data = response.json()
|
||||||
|
if response_data.get("status") != "success":
|
||||||
|
raise AyrshareAPIException(
|
||||||
|
f"Ayrshare API returned error: {response_data.get('message', 'Unknown error')}",
|
||||||
|
response.status_code,
|
||||||
|
)
|
||||||
|
|
||||||
|
# Return the first post from the response
|
||||||
|
return PostResponse(**response_data["posts"][0])
|
||||||
531
autogpt_platform/backend/backend/blocks/ayrshare/post.py
Normal file
531
autogpt_platform/backend/backend/blocks/ayrshare/post.py
Normal file
@@ -0,0 +1,531 @@
|
|||||||
|
import logging
|
||||||
|
from datetime import datetime
|
||||||
|
from typing import List, Optional
|
||||||
|
|
||||||
|
from pydantic import BaseModel, Field, SecretStr
|
||||||
|
|
||||||
|
from backend.blocks.ayrshare._api import (
|
||||||
|
AyrshareClient,
|
||||||
|
PostError,
|
||||||
|
PostResponse,
|
||||||
|
SocialPlatform,
|
||||||
|
)
|
||||||
|
from backend.data.block import Block, BlockCategory, BlockOutput, BlockSchema, BlockType
|
||||||
|
from backend.data.model import SchemaField
|
||||||
|
from backend.integrations.credentials_store import IntegrationCredentialsStore
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
creads_store = IntegrationCredentialsStore()
|
||||||
|
|
||||||
|
|
||||||
|
class RequestOutput(BaseModel):
|
||||||
|
"""Base output model for Ayrshare social media posts."""
|
||||||
|
|
||||||
|
status: str = Field(..., description="Status of the post")
|
||||||
|
id: str = Field(..., description="ID of the post")
|
||||||
|
refId: str = Field(..., description="Reference ID of the post")
|
||||||
|
profileTitle: str = Field(..., description="Title of the profile")
|
||||||
|
post: str = Field(..., description="The post text")
|
||||||
|
postIds: Optional[List[dict]] = Field(
|
||||||
|
description="IDs of the posts on each platform"
|
||||||
|
)
|
||||||
|
scheduleDate: Optional[str] = Field(description="Scheduled date of the post")
|
||||||
|
errors: Optional[List[str]] = Field(description="Any errors that occurred")
|
||||||
|
|
||||||
|
|
||||||
|
class AyrsharePostBlockBase(Block):
|
||||||
|
"""Base class for Ayrshare social media posting blocks."""
|
||||||
|
|
||||||
|
class Input(BlockSchema):
|
||||||
|
"""Base input model for Ayrshare social media posts."""
|
||||||
|
|
||||||
|
post: str = SchemaField(
|
||||||
|
description="The post text to be published", default="", advanced=False
|
||||||
|
)
|
||||||
|
media_urls: List[str] = SchemaField(
|
||||||
|
description="Optional list of media URLs to include. Set is_video in advanced settings to true if you want to upload videos.",
|
||||||
|
default_factory=list,
|
||||||
|
advanced=False,
|
||||||
|
)
|
||||||
|
is_video: bool = SchemaField(
|
||||||
|
description="Whether the media is a video", default=False, advanced=True
|
||||||
|
)
|
||||||
|
schedule_date: Optional[datetime] = SchemaField(
|
||||||
|
description="UTC datetime for scheduling (YYYY-MM-DDThh:mm:ssZ)",
|
||||||
|
default=None,
|
||||||
|
advanced=True,
|
||||||
|
)
|
||||||
|
disable_comments: bool = SchemaField(
|
||||||
|
description="Whether to disable comments", default=False, advanced=True
|
||||||
|
)
|
||||||
|
shorten_links: bool = SchemaField(
|
||||||
|
description="Whether to shorten links", default=False, advanced=True
|
||||||
|
)
|
||||||
|
|
||||||
|
unsplash: Optional[str] = SchemaField(
|
||||||
|
description="Unsplash image configuration", default=None, advanced=True
|
||||||
|
)
|
||||||
|
requires_approval: bool = SchemaField(
|
||||||
|
description="Whether to enable approval workflow",
|
||||||
|
default=False,
|
||||||
|
advanced=True,
|
||||||
|
)
|
||||||
|
random_post: bool = SchemaField(
|
||||||
|
description="Whether to generate random post text",
|
||||||
|
default=False,
|
||||||
|
advanced=True,
|
||||||
|
)
|
||||||
|
random_media_url: bool = SchemaField(
|
||||||
|
description="Whether to generate random media", default=False, advanced=True
|
||||||
|
)
|
||||||
|
notes: Optional[str] = SchemaField(
|
||||||
|
description="Additional notes for the post", default=None, advanced=True
|
||||||
|
)
|
||||||
|
|
||||||
|
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",
|
||||||
|
):
|
||||||
|
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=id,
|
||||||
|
# The description of the block, explaining what the block does.
|
||||||
|
description=description,
|
||||||
|
# The set of categories that the block belongs to.
|
||||||
|
# Each category is an instance of BlockCategory Enum.
|
||||||
|
categories={BlockCategory.SOCIAL},
|
||||||
|
# The type of block, this is used to determine the block type in the UI.
|
||||||
|
block_type=BlockType.AYRSHARE,
|
||||||
|
# The schema, defined as a Pydantic model, for the input data.
|
||||||
|
input_schema=AyrsharePostBlockBase.Input,
|
||||||
|
# The schema, defined as a Pydantic model, for the output data.
|
||||||
|
output_schema=AyrsharePostBlockBase.Output,
|
||||||
|
)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def create_client():
|
||||||
|
return AyrshareClient()
|
||||||
|
|
||||||
|
def _create_post(
|
||||||
|
self,
|
||||||
|
input_data: "AyrsharePostBlockBase.Input",
|
||||||
|
platforms: List[SocialPlatform],
|
||||||
|
profile_key: Optional[str] = None,
|
||||||
|
) -> PostResponse | PostError:
|
||||||
|
client = self.create_client()
|
||||||
|
"""Create a post on the specified platforms."""
|
||||||
|
iso_date = (
|
||||||
|
input_data.schedule_date.isoformat() if input_data.schedule_date else None
|
||||||
|
)
|
||||||
|
response = client.create_post(
|
||||||
|
post=input_data.post,
|
||||||
|
platforms=platforms,
|
||||||
|
media_urls=input_data.media_urls,
|
||||||
|
is_video=input_data.is_video,
|
||||||
|
schedule_date=iso_date,
|
||||||
|
disable_comments=input_data.disable_comments,
|
||||||
|
shorten_links=input_data.shorten_links,
|
||||||
|
unsplash=input_data.unsplash,
|
||||||
|
requires_approval=input_data.requires_approval,
|
||||||
|
random_post=input_data.random_post,
|
||||||
|
random_media_url=input_data.random_media_url,
|
||||||
|
notes=input_data.notes,
|
||||||
|
profile_key=profile_key,
|
||||||
|
)
|
||||||
|
return response
|
||||||
|
|
||||||
|
def run(
|
||||||
|
self,
|
||||||
|
input_data: "AyrsharePostBlockBase.Input",
|
||||||
|
*,
|
||||||
|
profile_key: SecretStr,
|
||||||
|
**kwargs,
|
||||||
|
) -> BlockOutput:
|
||||||
|
"""Run the block."""
|
||||||
|
platforms = [SocialPlatform.FACEBOOK]
|
||||||
|
|
||||||
|
if not profile_key:
|
||||||
|
yield "error", "Please Link a social account via Ayrshare"
|
||||||
|
return
|
||||||
|
|
||||||
|
post_result = self._create_post(
|
||||||
|
input_data, platforms=platforms, profile_key=profile_key.get_secret_value()
|
||||||
|
)
|
||||||
|
if isinstance(post_result, PostError):
|
||||||
|
yield "error", post_result.message
|
||||||
|
return
|
||||||
|
yield "post_result", post_result
|
||||||
|
|
||||||
|
|
||||||
|
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
|
||||||
|
|
||||||
|
|
||||||
|
AYRSHARE_NODE_IDS = [
|
||||||
|
PostToBlueskyBlock().id,
|
||||||
|
PostToFacebookBlock().id,
|
||||||
|
PostToXBlock().id,
|
||||||
|
PostToLinkedInBlock().id,
|
||||||
|
PostToInstagramBlock().id,
|
||||||
|
PostToYouTubeBlock().id,
|
||||||
|
PostToRedditBlock().id,
|
||||||
|
PostToTelegramBlock().id,
|
||||||
|
PostToGMBBlock().id,
|
||||||
|
PostToPinterestBlock().id,
|
||||||
|
PostToTikTokBlock().id,
|
||||||
|
]
|
||||||
@@ -1,12 +1,14 @@
|
|||||||
import json
|
import json
|
||||||
import logging
|
import logging
|
||||||
from enum import Enum
|
from enum import Enum
|
||||||
|
from io import BufferedReader
|
||||||
from typing import Any
|
from typing import Any
|
||||||
|
|
||||||
from requests.exceptions import HTTPError, RequestException
|
from requests.exceptions import HTTPError, RequestException
|
||||||
|
|
||||||
from backend.data.block import Block, BlockCategory, BlockOutput, BlockSchema
|
from backend.data.block import Block, BlockCategory, BlockOutput, BlockSchema
|
||||||
from backend.data.model import SchemaField
|
from backend.data.model import SchemaField
|
||||||
|
from backend.util.file import MediaFileType, get_exec_file_path, store_media_file
|
||||||
from backend.util.request import requests
|
from backend.util.request import requests
|
||||||
|
|
||||||
logger = logging.getLogger(name=__name__)
|
logger = logging.getLogger(name=__name__)
|
||||||
@@ -45,6 +47,10 @@ class SendWebRequestBlock(Block):
|
|||||||
description="The body of the request",
|
description="The body of the request",
|
||||||
default=None,
|
default=None,
|
||||||
)
|
)
|
||||||
|
files: dict[str, MediaFileType] = SchemaField(
|
||||||
|
description="File fields mapping to MediaFileType for multipart upload",
|
||||||
|
default_factory=dict,
|
||||||
|
)
|
||||||
|
|
||||||
class Output(BlockSchema):
|
class Output(BlockSchema):
|
||||||
response: object = SchemaField(description="The response from the server")
|
response: object = SchemaField(description="The response from the server")
|
||||||
@@ -61,7 +67,7 @@ class SendWebRequestBlock(Block):
|
|||||||
output_schema=SendWebRequestBlock.Output,
|
output_schema=SendWebRequestBlock.Output,
|
||||||
)
|
)
|
||||||
|
|
||||||
def run(self, input_data: Input, **kwargs) -> BlockOutput:
|
def run(self, input_data: Input, *, graph_exec_id: str, **kwargs) -> BlockOutput:
|
||||||
body = input_data.body
|
body = input_data.body
|
||||||
|
|
||||||
if input_data.json_format:
|
if input_data.json_format:
|
||||||
@@ -74,11 +80,31 @@ class SendWebRequestBlock(Block):
|
|||||||
# we should send it as plain text instead
|
# we should send it as plain text instead
|
||||||
input_data.json_format = False
|
input_data.json_format = False
|
||||||
|
|
||||||
|
# Prepare files for multipart upload using store_media_file
|
||||||
|
files: dict[str, BufferedReader] = {}
|
||||||
|
if input_data.files:
|
||||||
|
for field_name, media in input_data.files.items():
|
||||||
|
try:
|
||||||
|
rel_path = store_media_file(
|
||||||
|
graph_exec_id, media, return_content=False
|
||||||
|
)
|
||||||
|
abs_path = get_exec_file_path(graph_exec_id, rel_path)
|
||||||
|
files[field_name] = open(abs_path, "rb")
|
||||||
|
except Exception as e:
|
||||||
|
yield "error", f"Failed to prepare file '{field_name}': {e}"
|
||||||
|
for f in files.values():
|
||||||
|
try:
|
||||||
|
f.close()
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
return
|
||||||
|
|
||||||
try:
|
try:
|
||||||
response = requests.request(
|
response = requests.request(
|
||||||
input_data.method.value,
|
input_data.method.value,
|
||||||
input_data.url,
|
input_data.url,
|
||||||
headers=input_data.headers,
|
headers=input_data.headers,
|
||||||
|
files=files if files else None,
|
||||||
json=body if input_data.json_format else None,
|
json=body if input_data.json_format else None,
|
||||||
data=body if not input_data.json_format else None,
|
data=body if not input_data.json_format else None,
|
||||||
)
|
)
|
||||||
@@ -119,3 +145,11 @@ class SendWebRequestBlock(Block):
|
|||||||
except Exception as e:
|
except Exception as e:
|
||||||
# Catch any other unexpected exceptions
|
# Catch any other unexpected exceptions
|
||||||
yield "error", str(e)
|
yield "error", str(e)
|
||||||
|
|
||||||
|
finally:
|
||||||
|
# ensure cleanup of file handles
|
||||||
|
for f in files.values():
|
||||||
|
try:
|
||||||
|
f.close()
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|||||||
@@ -53,6 +53,7 @@ class BlockType(Enum):
|
|||||||
WEBHOOK_MANUAL = "Webhook (manual)"
|
WEBHOOK_MANUAL = "Webhook (manual)"
|
||||||
AGENT = "Agent"
|
AGENT = "Agent"
|
||||||
AI = "AI"
|
AI = "AI"
|
||||||
|
AYRSHARE = "Ayrshare"
|
||||||
|
|
||||||
|
|
||||||
class BlockCategory(Enum):
|
class BlockCategory(Enum):
|
||||||
@@ -76,6 +77,7 @@ class BlockCategory(Enum):
|
|||||||
PRODUCTIVITY = "Block that helps with productivity"
|
PRODUCTIVITY = "Block that helps with productivity"
|
||||||
ISSUE_TRACKING = "Block that helps with issue tracking"
|
ISSUE_TRACKING = "Block that helps with issue tracking"
|
||||||
MULTIMEDIA = "Block that interacts with multimedia content"
|
MULTIMEDIA = "Block that interacts with multimedia content"
|
||||||
|
MARKETING = "Block that helps with marketing"
|
||||||
|
|
||||||
def dict(self) -> dict[str, str]:
|
def dict(self) -> dict[str, str]:
|
||||||
return {"category": self.name, "description": self.value}
|
return {"category": self.name, "description": self.value}
|
||||||
|
|||||||
@@ -15,7 +15,6 @@ from typing import (
|
|||||||
Literal,
|
Literal,
|
||||||
Optional,
|
Optional,
|
||||||
Sequence,
|
Sequence,
|
||||||
TypedDict,
|
|
||||||
TypeVar,
|
TypeVar,
|
||||||
get_args,
|
get_args,
|
||||||
)
|
)
|
||||||
@@ -37,6 +36,7 @@ from pydantic_core import (
|
|||||||
ValidationError,
|
ValidationError,
|
||||||
core_schema,
|
core_schema,
|
||||||
)
|
)
|
||||||
|
from typing_extensions import TypedDict
|
||||||
|
|
||||||
from backend.integrations.providers import ProviderName
|
from backend.integrations.providers import ProviderName
|
||||||
from backend.util.settings import Secrets
|
from backend.util.settings import Secrets
|
||||||
@@ -189,7 +189,7 @@ def SchemaField(
|
|||||||
class _BaseCredentials(BaseModel):
|
class _BaseCredentials(BaseModel):
|
||||||
id: str = Field(default_factory=lambda: str(uuid4()))
|
id: str = Field(default_factory=lambda: str(uuid4()))
|
||||||
provider: str
|
provider: str
|
||||||
title: Optional[str]
|
title: Optional[str] = None
|
||||||
|
|
||||||
@field_serializer("*")
|
@field_serializer("*")
|
||||||
def dump_secret_strings(value: Any, _info):
|
def dump_secret_strings(value: Any, _info):
|
||||||
@@ -200,13 +200,13 @@ class _BaseCredentials(BaseModel):
|
|||||||
|
|
||||||
class OAuth2Credentials(_BaseCredentials):
|
class OAuth2Credentials(_BaseCredentials):
|
||||||
type: Literal["oauth2"] = "oauth2"
|
type: Literal["oauth2"] = "oauth2"
|
||||||
username: Optional[str]
|
username: Optional[str] = None
|
||||||
"""Username of the third-party service user that these credentials belong to"""
|
"""Username of the third-party service user that these credentials belong to"""
|
||||||
access_token: SecretStr
|
access_token: SecretStr
|
||||||
access_token_expires_at: Optional[int]
|
access_token_expires_at: Optional[int] = None
|
||||||
"""Unix timestamp (seconds) indicating when the access token expires (if at all)"""
|
"""Unix timestamp (seconds) indicating when the access token expires (if at all)"""
|
||||||
refresh_token: Optional[SecretStr]
|
refresh_token: Optional[SecretStr] = None
|
||||||
refresh_token_expires_at: Optional[int]
|
refresh_token_expires_at: Optional[int] = None
|
||||||
"""Unix timestamp (seconds) indicating when the refresh token expires (if at all)"""
|
"""Unix timestamp (seconds) indicating when the refresh token expires (if at all)"""
|
||||||
scopes: list[str]
|
scopes: list[str]
|
||||||
metadata: dict[str, Any] = Field(default_factory=dict)
|
metadata: dict[str, Any] = Field(default_factory=dict)
|
||||||
@@ -260,15 +260,32 @@ class OAuthState(BaseModel):
|
|||||||
|
|
||||||
class UserMetadata(BaseModel):
|
class UserMetadata(BaseModel):
|
||||||
integration_credentials: list[Credentials] = Field(default_factory=list)
|
integration_credentials: list[Credentials] = Field(default_factory=list)
|
||||||
|
"""⚠️ Deprecated; use `UserIntegrations.credentials` instead"""
|
||||||
integration_oauth_states: list[OAuthState] = Field(default_factory=list)
|
integration_oauth_states: list[OAuthState] = Field(default_factory=list)
|
||||||
|
"""⚠️ Deprecated; use `UserIntegrations.oauth_states` instead"""
|
||||||
|
|
||||||
|
|
||||||
class UserMetadataRaw(TypedDict, total=False):
|
class UserMetadataRaw(TypedDict, total=False):
|
||||||
integration_credentials: list[dict]
|
integration_credentials: list[dict]
|
||||||
|
"""⚠️ Deprecated; use `UserIntegrations.credentials` instead"""
|
||||||
integration_oauth_states: list[dict]
|
integration_oauth_states: list[dict]
|
||||||
|
"""⚠️ Deprecated; use `UserIntegrations.oauth_states` instead"""
|
||||||
|
|
||||||
|
|
||||||
class UserIntegrations(BaseModel):
|
class UserIntegrations(BaseModel):
|
||||||
|
|
||||||
|
class ManagedCredentials(BaseModel):
|
||||||
|
"""Integration credentials managed by us, rather than by the user"""
|
||||||
|
|
||||||
|
ayrshare_profile_key: Optional[SecretStr] = None
|
||||||
|
|
||||||
|
@field_serializer("*")
|
||||||
|
def dump_secret_strings(value: Any, _info):
|
||||||
|
if isinstance(value, SecretStr):
|
||||||
|
return value.get_secret_value()
|
||||||
|
return value
|
||||||
|
|
||||||
|
managed_credentials: ManagedCredentials = Field(default_factory=ManagedCredentials)
|
||||||
credentials: list[Credentials] = Field(default_factory=list)
|
credentials: list[Credentials] = Field(default_factory=list)
|
||||||
oauth_states: list[OAuthState] = Field(default_factory=list)
|
oauth_states: list[OAuthState] = Field(default_factory=list)
|
||||||
|
|
||||||
|
|||||||
@@ -124,7 +124,7 @@ async def get_user_integrations(user_id: str) -> UserIntegrations:
|
|||||||
|
|
||||||
|
|
||||||
async def update_user_integrations(user_id: str, data: UserIntegrations):
|
async def update_user_integrations(user_id: str, data: UserIntegrations):
|
||||||
encrypted_data = JSONCryptor().encrypt(data.model_dump())
|
encrypted_data = JSONCryptor().encrypt(data.model_dump(exclude_none=True))
|
||||||
await User.prisma().update(
|
await User.prisma().update(
|
||||||
where={"id": user_id},
|
where={"id": user_id},
|
||||||
data={"integrations": encrypted_data},
|
data={"integrations": encrypted_data},
|
||||||
|
|||||||
@@ -38,6 +38,7 @@ from autogpt_libs.utils.cache import thread_cached
|
|||||||
from prometheus_client import Gauge, start_http_server
|
from prometheus_client import Gauge, start_http_server
|
||||||
|
|
||||||
from backend.blocks.agent import AgentExecutorBlock
|
from backend.blocks.agent import AgentExecutorBlock
|
||||||
|
from backend.blocks.ayrshare.post import AYRSHARE_NODE_IDS
|
||||||
from backend.data import redis
|
from backend.data import redis
|
||||||
from backend.data.block import BlockData, BlockInput, BlockSchema, get_block
|
from backend.data.block import BlockData, BlockInput, BlockSchema, get_block
|
||||||
from backend.data.credit import UsageTransactionMetadata
|
from backend.data.credit import UsageTransactionMetadata
|
||||||
@@ -217,6 +218,10 @@ def execute_node(
|
|||||||
credentials, creds_lock = creds_manager.acquire(user_id, credentials_meta.id)
|
credentials, creds_lock = creds_manager.acquire(user_id, credentials_meta.id)
|
||||||
extra_exec_kwargs[field_name] = credentials
|
extra_exec_kwargs[field_name] = credentials
|
||||||
|
|
||||||
|
if node_block.id in AYRSHARE_NODE_IDS:
|
||||||
|
profile_key = creds_manager.store.get_ayrshare_profile_key(user_id)
|
||||||
|
extra_exec_kwargs["profile_key"] = profile_key
|
||||||
|
|
||||||
output_size = 0
|
output_size = 0
|
||||||
try:
|
try:
|
||||||
outputs: dict[str, Any] = {}
|
outputs: dict[str, Any] = {}
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import base64
|
import base64
|
||||||
import hashlib
|
import hashlib
|
||||||
import secrets
|
import secrets
|
||||||
|
from contextlib import contextmanager
|
||||||
from datetime import datetime, timedelta, timezone
|
from datetime import datetime, timedelta, timezone
|
||||||
from typing import TYPE_CHECKING, Optional
|
from typing import TYPE_CHECKING, Optional
|
||||||
|
|
||||||
@@ -177,6 +178,7 @@ zerobounce_credentials = APIKeyCredentials(
|
|||||||
expires_at=None,
|
expires_at=None,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
llama_api_credentials = APIKeyCredentials(
|
llama_api_credentials = APIKeyCredentials(
|
||||||
id="d44045af-1c33-4833-9e19-752313214de2",
|
id="d44045af-1c33-4833-9e19-752313214de2",
|
||||||
provider="llama_api",
|
provider="llama_api",
|
||||||
@@ -224,6 +226,8 @@ class IntegrationCredentialsStore:
|
|||||||
|
|
||||||
return get_service_client(DatabaseManagerClient)
|
return get_service_client(DatabaseManagerClient)
|
||||||
|
|
||||||
|
# =============== USER-MANAGED CREDENTIALS =============== #
|
||||||
|
|
||||||
def add_creds(self, user_id: str, credentials: Credentials) -> None:
|
def add_creds(self, user_id: str, credentials: Credentials) -> None:
|
||||||
with self.locked_user_integrations(user_id):
|
with self.locked_user_integrations(user_id):
|
||||||
if self.get_creds_by_id(user_id, credentials.id):
|
if self.get_creds_by_id(user_id, credentials.id):
|
||||||
@@ -282,6 +286,8 @@ class IntegrationCredentialsStore:
|
|||||||
all_credentials.append(zerobounce_credentials)
|
all_credentials.append(zerobounce_credentials)
|
||||||
if settings.secrets.google_maps_api_key:
|
if settings.secrets.google_maps_api_key:
|
||||||
all_credentials.append(google_maps_credentials)
|
all_credentials.append(google_maps_credentials)
|
||||||
|
if settings.secrets.llama_api_key:
|
||||||
|
all_credentials.append(llama_api_credentials)
|
||||||
return all_credentials
|
return all_credentials
|
||||||
|
|
||||||
def get_creds_by_id(self, user_id: str, credentials_id: str) -> Credentials | None:
|
def get_creds_by_id(self, user_id: str, credentials_id: str) -> Credentials | None:
|
||||||
@@ -337,6 +343,19 @@ class IntegrationCredentialsStore:
|
|||||||
]
|
]
|
||||||
self._set_user_integration_creds(user_id, filtered_credentials)
|
self._set_user_integration_creds(user_id, filtered_credentials)
|
||||||
|
|
||||||
|
# ============== SYSTEM-MANAGED CREDENTIALS ============== #
|
||||||
|
|
||||||
|
def get_ayrshare_profile_key(self, user_id: str) -> SecretStr | None:
|
||||||
|
managed_user_creds = self._get_user_integrations(user_id).managed_credentials
|
||||||
|
return managed_user_creds.ayrshare_profile_key
|
||||||
|
|
||||||
|
def set_ayrshare_profile_key(self, user_id: str, profile_key: str) -> None:
|
||||||
|
_profile_key = SecretStr(profile_key)
|
||||||
|
with self.edit_user_integrations(user_id) as user_integrations:
|
||||||
|
user_integrations.managed_credentials.ayrshare_profile_key = _profile_key
|
||||||
|
|
||||||
|
# ===================== OAUTH STATES ===================== #
|
||||||
|
|
||||||
def store_state_token(
|
def store_state_token(
|
||||||
self, user_id: str, provider: str, scopes: list[str], use_pkce: bool = False
|
self, user_id: str, provider: str, scopes: list[str], use_pkce: bool = False
|
||||||
) -> tuple[str, str]:
|
) -> tuple[str, str]:
|
||||||
@@ -353,16 +372,8 @@ class IntegrationCredentialsStore:
|
|||||||
scopes=scopes,
|
scopes=scopes,
|
||||||
)
|
)
|
||||||
|
|
||||||
with self.locked_user_integrations(user_id):
|
with self.edit_user_integrations(user_id) as user_integrations:
|
||||||
|
user_integrations.oauth_states.append(state)
|
||||||
user_integrations = self._get_user_integrations(user_id)
|
|
||||||
oauth_states = user_integrations.oauth_states
|
|
||||||
oauth_states.append(state)
|
|
||||||
user_integrations.oauth_states = oauth_states
|
|
||||||
|
|
||||||
self.db_manager.update_user_integrations(
|
|
||||||
user_id=user_id, data=user_integrations
|
|
||||||
)
|
|
||||||
|
|
||||||
return token, code_challenge
|
return token, code_challenge
|
||||||
|
|
||||||
@@ -404,6 +415,17 @@ class IntegrationCredentialsStore:
|
|||||||
|
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
# =================== GET/SET HELPERS =================== #
|
||||||
|
|
||||||
|
@contextmanager
|
||||||
|
def edit_user_integrations(self, user_id: str):
|
||||||
|
with self.locked_user_integrations(user_id):
|
||||||
|
user_integrations = self._get_user_integrations(user_id)
|
||||||
|
yield user_integrations # yield to allow edits
|
||||||
|
self.db_manager.update_user_integrations(
|
||||||
|
user_id=user_id, data=user_integrations
|
||||||
|
)
|
||||||
|
|
||||||
def _set_user_integration_creds(
|
def _set_user_integration_creds(
|
||||||
self, user_id: str, credentials: list[Credentials]
|
self, user_id: str, credentials: list[Credentials]
|
||||||
) -> None:
|
) -> None:
|
||||||
|
|||||||
@@ -1,11 +1,13 @@
|
|||||||
import asyncio
|
import asyncio
|
||||||
import logging
|
import logging
|
||||||
|
from datetime import datetime, timedelta, timezone
|
||||||
from typing import TYPE_CHECKING, Annotated, Awaitable, Literal
|
from typing import TYPE_CHECKING, Annotated, Awaitable, Literal
|
||||||
|
|
||||||
from fastapi import APIRouter, Body, Depends, HTTPException, Path, Query, Request
|
from fastapi import APIRouter, Body, Depends, HTTPException, Path, Query, Request
|
||||||
from pydantic import BaseModel, Field
|
from pydantic import BaseModel, Field, SecretStr
|
||||||
from starlette.status import HTTP_404_NOT_FOUND
|
from starlette.status import HTTP_404_NOT_FOUND
|
||||||
|
|
||||||
|
from backend.blocks.ayrshare._api import AyrshareClient, PostError, SocialPlatform
|
||||||
from backend.data.graph import set_node_webhook
|
from backend.data.graph import set_node_webhook
|
||||||
from backend.data.integrations import (
|
from backend.data.integrations import (
|
||||||
WebhookEvent,
|
WebhookEvent,
|
||||||
@@ -416,3 +418,72 @@ def _get_provider_oauth_handler(
|
|||||||
client_secret=client_secret,
|
client_secret=client_secret,
|
||||||
redirect_uri=f"{frontend_base_url}/auth/integrations/oauth_callback",
|
redirect_uri=f"{frontend_base_url}/auth/integrations/oauth_callback",
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@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)
|
||||||
|
return {"sso_url": jwt_response.url, "expire_at": expire_at.isoformat()}
|
||||||
|
|||||||
@@ -82,6 +82,7 @@ async def test_get_library_agents(mocker):
|
|||||||
|
|
||||||
|
|
||||||
@pytest.mark.asyncio(loop_scope="session")
|
@pytest.mark.asyncio(loop_scope="session")
|
||||||
|
@pytest.mark.skip(reason="Test not implemented yet")
|
||||||
async def test_add_agent_to_library(mocker):
|
async def test_add_agent_to_library(mocker):
|
||||||
await connect()
|
await connect()
|
||||||
# Mock data
|
# Mock data
|
||||||
|
|||||||
@@ -438,7 +438,8 @@ class Secrets(UpdateTrackingModel["Secrets"], BaseSettings):
|
|||||||
apollo_api_key: str = Field(default="", description="Apollo API Key")
|
apollo_api_key: str = Field(default="", description="Apollo API Key")
|
||||||
smartlead_api_key: str = Field(default="", description="SmartLead API Key")
|
smartlead_api_key: str = Field(default="", description="SmartLead API Key")
|
||||||
zerobounce_api_key: str = Field(default="", description="ZeroBounce API Key")
|
zerobounce_api_key: str = Field(default="", description="ZeroBounce API Key")
|
||||||
|
ayrshare_api_key: str = Field(default="", description="Ayrshare API Key")
|
||||||
|
ayrshare_jwt_key: str = Field(default="", description="Ayrshare private Key")
|
||||||
# Add more secret fields as needed
|
# Add more secret fields as needed
|
||||||
|
|
||||||
model_config = SettingsConfigDict(
|
model_config = SettingsConfigDict(
|
||||||
|
|||||||
@@ -53,7 +53,7 @@ import {
|
|||||||
CopyIcon,
|
CopyIcon,
|
||||||
ExitIcon,
|
ExitIcon,
|
||||||
} from "@radix-ui/react-icons";
|
} from "@radix-ui/react-icons";
|
||||||
|
import { FaKey } from "react-icons/fa";
|
||||||
import useCredits from "@/hooks/useCredits";
|
import useCredits from "@/hooks/useCredits";
|
||||||
|
|
||||||
export type ConnectionData = Array<{
|
export type ConnectionData = Array<{
|
||||||
@@ -116,6 +116,8 @@ export const CustomNode = React.memo(
|
|||||||
const flowContext = useContext(FlowContext);
|
const flowContext = useContext(FlowContext);
|
||||||
const api = useBackendAPI();
|
const api = useBackendAPI();
|
||||||
const { formatCredits } = useCredits();
|
const { formatCredits } = useCredits();
|
||||||
|
const [isLoading, setIsLoading] = useState(false);
|
||||||
|
|
||||||
let nodeFlowId = "";
|
let nodeFlowId = "";
|
||||||
|
|
||||||
if (data.uiType === BlockUIType.AGENT) {
|
if (data.uiType === BlockUIType.AGENT) {
|
||||||
@@ -249,6 +251,55 @@ export const CustomNode = React.memo(
|
|||||||
return renderHandles(schema.properties);
|
return renderHandles(schema.properties);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const generateAyrshareSSOHandles = (
|
||||||
|
api: ReturnType<typeof useBackendAPI>,
|
||||||
|
) => {
|
||||||
|
const handleSSOLogin = async () => {
|
||||||
|
setIsLoading(true);
|
||||||
|
try {
|
||||||
|
const { sso_url } = await api.getAyrshareSSOUrl();
|
||||||
|
const popup = window.open(sso_url, "_blank", "popup=true");
|
||||||
|
if (!popup) {
|
||||||
|
throw new Error(
|
||||||
|
"Failed to open popup window. Please allow popups for this site.",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error getting SSO URL:", error);
|
||||||
|
} finally {
|
||||||
|
setIsLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex flex-col gap-2">
|
||||||
|
<Button
|
||||||
|
type="button"
|
||||||
|
variant="outline"
|
||||||
|
className="w-full"
|
||||||
|
onClick={handleSSOLogin}
|
||||||
|
disabled={isLoading}
|
||||||
|
>
|
||||||
|
{isLoading ? (
|
||||||
|
"Loading..."
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<FaKey className="mr-2 h-4 w-4" />
|
||||||
|
Connect Social Media Accounts
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</Button>
|
||||||
|
<NodeHandle
|
||||||
|
title="SSO Token"
|
||||||
|
keyName="sso_token"
|
||||||
|
isConnected={false}
|
||||||
|
schema={{ type: "string" }}
|
||||||
|
side="right"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
const generateInputHandles = (
|
const generateInputHandles = (
|
||||||
schema: BlockIORootSchema,
|
schema: BlockIORootSchema,
|
||||||
nodeType: BlockUIType,
|
nodeType: BlockUIType,
|
||||||
@@ -826,8 +877,18 @@ export const CustomNode = React.memo(
|
|||||||
(A Webhook URL will be generated when you save the agent)
|
(A Webhook URL will be generated when you save the agent)
|
||||||
</p>
|
</p>
|
||||||
))}
|
))}
|
||||||
{data.inputSchema &&
|
{data.uiType === BlockUIType.AYRSHARE ? (
|
||||||
generateInputHandles(data.inputSchema, data.uiType)}
|
<>
|
||||||
|
{generateAyrshareSSOHandles(api)}
|
||||||
|
{generateInputHandles(
|
||||||
|
data.inputSchema,
|
||||||
|
BlockUIType.STANDARD,
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
data.inputSchema &&
|
||||||
|
generateInputHandles(data.inputSchema, data.uiType)
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
|
|||||||
@@ -100,8 +100,9 @@ export default function CredentialsProvider({
|
|||||||
}: {
|
}: {
|
||||||
children: React.ReactNode;
|
children: React.ReactNode;
|
||||||
}) {
|
}) {
|
||||||
const [providers, setProviders] =
|
const [providers, setProviders] = useState<CredentialsProvidersContextType>(
|
||||||
useState<CredentialsProvidersContextType | null>(null);
|
{},
|
||||||
|
);
|
||||||
const api = useBackendAPI();
|
const api = useBackendAPI();
|
||||||
|
|
||||||
const addCredentials = useCallback(
|
const addCredentials = useCallback(
|
||||||
|
|||||||
@@ -669,6 +669,10 @@ export default class BackendAPI {
|
|||||||
await this._request("DELETE", `/library/presets/${presetId}`);
|
await this._request("DELETE", `/library/presets/${presetId}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
getAyrshareSSOUrl(): Promise<{ sso_url: string; expire_at: string }> {
|
||||||
|
return this._get("/integrations/ayrshare/sso_url");
|
||||||
|
}
|
||||||
|
|
||||||
executeLibraryAgentPreset(
|
executeLibraryAgentPreset(
|
||||||
presetId: string,
|
presetId: string,
|
||||||
graphId: GraphID,
|
graphId: GraphID,
|
||||||
|
|||||||
@@ -580,6 +580,7 @@ export enum BlockUIType {
|
|||||||
WEBHOOK_MANUAL = "Webhook (manual)",
|
WEBHOOK_MANUAL = "Webhook (manual)",
|
||||||
AGENT = "Agent",
|
AGENT = "Agent",
|
||||||
AI = "AI",
|
AI = "AI",
|
||||||
|
AYRSHARE = "Ayrshare",
|
||||||
}
|
}
|
||||||
|
|
||||||
export enum SpecialBlockID {
|
export enum SpecialBlockID {
|
||||||
|
|||||||
Reference in New Issue
Block a user