Spaces:
Sleeping
Sleeping
Upload 5 files
Browse files- agents.py +170 -0
- app.py +472 -0
- helpers.py +193 -0
- requirements.txt +10 -0
- test2.py +306 -0
agents.py
ADDED
@@ -0,0 +1,170 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
from langchain_google_genai import GoogleGenerativeAI
|
2 |
+
from langchain_core.prompts import ChatPromptTemplate
|
3 |
+
from langchain.chains import LLMChain
|
4 |
+
|
5 |
+
class SocialMediaAgents:
|
6 |
+
PLATFORM_LIMITS = {
|
7 |
+
"twitter": {"chars": 280, "words": None},
|
8 |
+
"instagram": {"chars": None, "words": 400},
|
9 |
+
"linkedin": {"chars": None, "words": 600},
|
10 |
+
"facebook": {"chars": None, "words": 1000}
|
11 |
+
}
|
12 |
+
|
13 |
+
def __init__(self, api_key: str):
|
14 |
+
"""Initialize the agent with a Google API key."""
|
15 |
+
self.llm = GoogleGenerativeAI(model="gemini-1.5-flash", google_api_key=api_key)
|
16 |
+
|
17 |
+
def _create_chain(self, template: str) -> LLMChain:
|
18 |
+
"""Create an LLM chain with the given prompt template."""
|
19 |
+
prompt = ChatPromptTemplate.from_template(template)
|
20 |
+
return LLMChain(llm=self.llm, prompt=prompt)
|
21 |
+
|
22 |
+
def _enforce_limits(self, text: str, platform: str) -> str:
|
23 |
+
"""Enforce platform-specific character or word limits."""
|
24 |
+
limits = self.PLATFORM_LIMITS[platform.lower()]
|
25 |
+
if limits["chars"] and len(text) > limits["chars"]:
|
26 |
+
return text[:limits["chars"]-3] + "..."
|
27 |
+
if limits["words"]:
|
28 |
+
words = text.split()
|
29 |
+
if len(words) > limits["words"]:
|
30 |
+
return " ".join(words[:limits["words"]]) + "..."
|
31 |
+
return text
|
32 |
+
|
33 |
+
# def twitter_transform(self, title: str, description: str) -> dict:
|
34 |
+
# """Transform content for Twitter."""
|
35 |
+
# link = "https://www.eye-on.ai/podcast-archive"
|
36 |
+
# template = """Transform this into a Twitter post (280 characters max):
|
37 |
+
# - Attention-grabbing message
|
38 |
+
# - 1-2 relevant hashtags
|
39 |
+
# - Essential information only
|
40 |
+
|
41 |
+
# Format output EXACTLY like this:
|
42 |
+
# New Title: [transformed title]
|
43 |
+
# ---
|
44 |
+
# New Description: [transformed description]
|
45 |
+
|
46 |
+
# add this line after descripttion and make link clickable listen to full podcast on {link}
|
47 |
+
|
48 |
+
# Original Content:
|
49 |
+
# Title: {title}
|
50 |
+
# Description: {description}"""
|
51 |
+
# chain = self._create_chain(template)
|
52 |
+
# response = chain.invoke({"title": title, "description": description, "link": link})
|
53 |
+
|
54 |
+
def twitter_transform(self, title: str, description: str,link:str) -> dict:
|
55 |
+
"""Transform content for Twitter with a clickable link and within 280 characters."""
|
56 |
+
template = """
|
57 |
+
Transform this into a Twitter post (max 280 characters total):
|
58 |
+
- Create an attention-grabbing single-line tweet using key info from the title and description
|
59 |
+
- Include 1-2 relevant hashtags
|
60 |
+
- End with this line exactly: Listen to full podcast: {link}
|
61 |
+
- Ensure the ENTIRE result is no more than 280 characters TOTAL (including the link line)
|
62 |
+
- if character more than 280 characters manage limit and exclude description character
|
63 |
+
- Don't short {link} i want full link
|
64 |
+
Return in this format:
|
65 |
+
New Title: [transformed title]
|
66 |
+
---
|
67 |
+
New Description: [tweet content]
|
68 |
+
|
69 |
+
Original Content:
|
70 |
+
Title: {title}
|
71 |
+
Description: {description}
|
72 |
+
"""
|
73 |
+
chain = self._create_chain(template)
|
74 |
+
response = chain.invoke({"title": title, "description": description, "link": link})
|
75 |
+
|
76 |
+
|
77 |
+
|
78 |
+
parts = response['text'].split('---')
|
79 |
+
result = {
|
80 |
+
"new_title": parts[0].replace('New Title:', '').strip(),
|
81 |
+
"new_description": parts[1].replace('New Description:', '').strip()
|
82 |
+
}
|
83 |
+
combined_text = f"{result['new_title']} {result['new_description']}"
|
84 |
+
limited_text = self._enforce_limits(combined_text, "twitter")
|
85 |
+
if len(limited_text) < len(combined_text):
|
86 |
+
result['new_title'] = ""
|
87 |
+
result['new_description'] = limited_text
|
88 |
+
return result
|
89 |
+
|
90 |
+
def instagram_transform(self, title: str, description: str) -> dict:
|
91 |
+
"""Transform content for Instagram."""
|
92 |
+
template = """Transform this into an Instagram post (400 words max):
|
93 |
+
- Catchy title with relevant emojis
|
94 |
+
- Engaging description
|
95 |
+
- 3-5 relevant hashtags
|
96 |
+
|
97 |
+
Format output EXACTLY like this:
|
98 |
+
New Title: [transformed title]
|
99 |
+
---
|
100 |
+
New Description: [transformed description]
|
101 |
+
|
102 |
+
Original Content:
|
103 |
+
Title: {title}
|
104 |
+
Description: {description}"""
|
105 |
+
chain = self._create_chain(template)
|
106 |
+
response = chain.invoke({"title": title, "description": description})
|
107 |
+
parts = response['text'].split('---')
|
108 |
+
result = {
|
109 |
+
"new_title": parts[0].replace('New Title:', '').strip(),
|
110 |
+
"new_description": parts[1].replace('New Description:', '').strip()
|
111 |
+
}
|
112 |
+
result['new_description'] = self._enforce_limits(result['new_description'], "instagram")
|
113 |
+
return result
|
114 |
+
|
115 |
+
def linkedin_transform(self, title: str, description: str,link) -> dict:
|
116 |
+
"""Transform content for LinkedIn."""
|
117 |
+
|
118 |
+
template = """Transform this into a LinkedIn post (600 words max):
|
119 |
+
- Professional title
|
120 |
+
- Detailed description with business insights
|
121 |
+
- 2-3 relevant hashtags
|
122 |
+
- Professional tone
|
123 |
+
- End with this line exactly: Listen to full podcast: {link}
|
124 |
+
- Don't change link format and words.
|
125 |
+
- Ensure the ENTIRE result is no more than 600 words TOTAL (including the link line)
|
126 |
+
- if character more than 600 words manage limit and exclude description character
|
127 |
+
|
128 |
+
Format output EXACTLY like this:
|
129 |
+
New Title: [transformed title]
|
130 |
+
---
|
131 |
+
New Description: [transformed description]
|
132 |
+
|
133 |
+
Original Content:
|
134 |
+
Title: {title}
|
135 |
+
Description: {description}"""
|
136 |
+
chain = self._create_chain(template)
|
137 |
+
response = chain.invoke({"title": title, "description": description, "link": link})
|
138 |
+
parts = response['text'].split('---')
|
139 |
+
result = {
|
140 |
+
"new_title": parts[0].replace('New Title:', '').strip(),
|
141 |
+
"new_description": parts[1].replace('New Description:', '').strip()
|
142 |
+
}
|
143 |
+
result['new_description'] = self._enforce_limits(result['new_description'], "linkedin")
|
144 |
+
return result
|
145 |
+
|
146 |
+
def facebook_transform(self, title: str, description: str) -> dict:
|
147 |
+
"""Transform content for Facebook."""
|
148 |
+
template = """Transform this into a Facebook post (1000 words max):
|
149 |
+
- Engaging title
|
150 |
+
- Conversational description
|
151 |
+
- Call to action for engagement
|
152 |
+
- 1-2 relevant hashtags
|
153 |
+
|
154 |
+
Format output EXACTLY like this:
|
155 |
+
New Title: [transformed title]
|
156 |
+
---
|
157 |
+
New Description: [transformed description]
|
158 |
+
|
159 |
+
Original Content:
|
160 |
+
Title: {title}
|
161 |
+
Description: {description}"""
|
162 |
+
chain = self._create_chain(template)
|
163 |
+
response = chain.invoke({"title": title, "description": description})
|
164 |
+
parts = response['text'].split('---')
|
165 |
+
result = {
|
166 |
+
"new_title": parts[0].replace('New Title:', '').strip(),
|
167 |
+
"new_description": parts[1].replace('New Description:', '').strip()
|
168 |
+
}
|
169 |
+
result['new_description'] = self._enforce_limits(result['new_description'], "facebook")
|
170 |
+
return result
|
app.py
ADDED
@@ -0,0 +1,472 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
from flask import Flask, redirect, url_for, request, render_template, session
|
2 |
+
from apscheduler.schedulers.background import BackgroundScheduler
|
3 |
+
import requests
|
4 |
+
from datetime import datetime, timedelta
|
5 |
+
import tweepy
|
6 |
+
from agents import SocialMediaAgents # Assuming this is your agents.py file
|
7 |
+
import feedparser
|
8 |
+
from helpers import post_to_linkedin, post_to_twitter, extract_image_url
|
9 |
+
import random
|
10 |
+
import uuid
|
11 |
+
from dotenv import load_dotenv
|
12 |
+
import os
|
13 |
+
import atexit
|
14 |
+
import json
|
15 |
+
|
16 |
+
load_dotenv()
|
17 |
+
|
18 |
+
ngrok_link = os.getenv("Ngrok_Link")
|
19 |
+
|
20 |
+
app = Flask(__name__)
|
21 |
+
app.secret_key = '12345678765' # Replace with a secure key
|
22 |
+
|
23 |
+
scheduler = BackgroundScheduler()
|
24 |
+
scheduler.start()
|
25 |
+
|
26 |
+
api_key = os.getenv("Gemini_key")
|
27 |
+
|
28 |
+
agents = SocialMediaAgents(api_key)
|
29 |
+
|
30 |
+
LINKEDIN_CLIENT_ID = os.getenv("LINKEDIN_CLIENT_ID")
|
31 |
+
LINKEDIN_CLIENT_SECRET = os.getenv("LINKEDIN_CLIENT_SECRET")
|
32 |
+
TWITTER_CLIENT_ID = os.getenv("TWITTER_CLIENT_ID")
|
33 |
+
TWITTER_CLIENT_SECRET = os.getenv("TWITTER_CLIENT_SECRET")
|
34 |
+
|
35 |
+
posts = []
|
36 |
+
temp_posts = {}
|
37 |
+
|
38 |
+
@app.route('/')
|
39 |
+
def home():
|
40 |
+
connected_platforms = {
|
41 |
+
'linkedin': 'linkedin_access_token' in session and 'linkedin_id' in session,
|
42 |
+
'twitter': 'twitter_access_token' in session and 'twitter_access_token_secret' in session,
|
43 |
+
|
44 |
+
}
|
45 |
+
name ={
|
46 |
+
'name':session.get('linkedin_name'),
|
47 |
+
'tw_name':session.get('twitter_name')
|
48 |
+
}
|
49 |
+
print(name['tw_name'])
|
50 |
+
return render_template('home.html', connected_platforms=connected_platforms , name = name)
|
51 |
+
|
52 |
+
@app.route('/connect_all')
|
53 |
+
def connect_all():
|
54 |
+
session['connect_all'] = True
|
55 |
+
return redirect(url_for('linkedin_auth'))
|
56 |
+
|
57 |
+
@app.route('/linkedin/auth')
|
58 |
+
def linkedin_auth():
|
59 |
+
if "linkedin_access_token" not in session:
|
60 |
+
redirect_uri = f'{ngrok_link}/linkedin/callback'
|
61 |
+
scope = 'openid profile w_member_social'
|
62 |
+
auth_url = (
|
63 |
+
f'https://www.linkedin.com/oauth/v2/authorization?'
|
64 |
+
f'response_type=code&client_id={LINKEDIN_CLIENT_ID}&redirect_uri={redirect_uri}&'
|
65 |
+
f'scope={scope}&state=randomstring'
|
66 |
+
)
|
67 |
+
return redirect(auth_url)
|
68 |
+
|
69 |
+
@app.route('/linkedin/callback')
|
70 |
+
def linkedin_callback():
|
71 |
+
code = request.args.get('code')
|
72 |
+
if not code:
|
73 |
+
return "Error: No authorization code provided"
|
74 |
+
token_url = 'https://www.linkedin.com/oauth/v2/accessToken'
|
75 |
+
|
76 |
+
|
77 |
+
|
78 |
+
data = {
|
79 |
+
'grant_type': 'authorization_code',
|
80 |
+
'code': code,
|
81 |
+
'redirect_uri': f'{ngrok_link}/linkedin/callback',
|
82 |
+
'client_id': LINKEDIN_CLIENT_ID,
|
83 |
+
'client_secret': LINKEDIN_CLIENT_SECRET
|
84 |
+
}
|
85 |
+
response = requests.post(token_url, data=data)
|
86 |
+
if response.status_code != 200:
|
87 |
+
return "Error: Could not get LinkedIn access token"
|
88 |
+
token_data = response.json()
|
89 |
+
session['linkedin_access_token'] = token_data.get('access_token')
|
90 |
+
profile_url = 'https://api.linkedin.com/v2/userinfo'
|
91 |
+
headers = {'Authorization': f'Bearer {session["linkedin_access_token"]}'}
|
92 |
+
profile_response = requests.get(profile_url, headers=headers)
|
93 |
+
|
94 |
+
|
95 |
+
if profile_response.status_code != 200:
|
96 |
+
return "Error: Could not fetch LinkedIn profile"
|
97 |
+
user_info = profile_response.json()
|
98 |
+
session['linkedin_id'] = user_info.get('sub')
|
99 |
+
|
100 |
+
print("here333333333",user_info)
|
101 |
+
session['linkedin_name'] = user_info['name']
|
102 |
+
if session.get('connect_all') and 'twitter_access_token' not in session:
|
103 |
+
return redirect(url_for('twitter_auth'))
|
104 |
+
return redirect(url_for('home'))
|
105 |
+
|
106 |
+
@app.route('/twitter/auth')
|
107 |
+
def twitter_auth():
|
108 |
+
auth = tweepy.OAuth1UserHandler(TWITTER_CLIENT_ID, TWITTER_CLIENT_SECRET, f'{ngrok_link}/twitter/callback')
|
109 |
+
try:
|
110 |
+
redirect_url = auth.get_authorization_url()
|
111 |
+
session['request_token'] = auth.request_token
|
112 |
+
return redirect(redirect_url)
|
113 |
+
except tweepy.TweepyException as e:
|
114 |
+
return f"Error starting Twitter auth: {e}"
|
115 |
+
|
116 |
+
# @app.route('/twitter/callback')
|
117 |
+
# def twitter_callback():
|
118 |
+
# request_token = session.pop('request_token', None)
|
119 |
+
# if not request_token:
|
120 |
+
# return "Error: Request token not found in session. <a href='/twitter/auth'>Please try logging in again</a>."
|
121 |
+
# verifier = request.args.get('oauth_verifier')
|
122 |
+
# if not verifier:
|
123 |
+
# return "Error: No OAuth verifier provided"
|
124 |
+
# auth = tweepy.OAuth1UserHandler(TWITTER_CLIENT_ID, TWITTER_CLIENT_SECRET)
|
125 |
+
# auth.request_token = request_token
|
126 |
+
# try:
|
127 |
+
# auth.get_access_token(verifier)
|
128 |
+
# session['twitter_access_token'] = auth.access_token
|
129 |
+
# session['twitter_access_token_secret'] = auth.access_token_secret
|
130 |
+
# session.pop('connect_all', None)
|
131 |
+
# return redirect(url_for('home'))
|
132 |
+
# except tweepy.TweepyException as e:
|
133 |
+
# return f"Twitter authorization failed: {e}"
|
134 |
+
|
135 |
+
|
136 |
+
|
137 |
+
|
138 |
+
#============== Additional ============#######
|
139 |
+
|
140 |
+
|
141 |
+
@app.route('/twitter/callback')
|
142 |
+
def twitter_callback():
|
143 |
+
request_token = session.pop('request_token', None)
|
144 |
+
if not request_token:
|
145 |
+
return "Error: Request token not found in session. <a href='/twitter/auth'>Please try logging in again</a>."
|
146 |
+
|
147 |
+
verifier = request.args.get('oauth_verifier')
|
148 |
+
if not verifier:
|
149 |
+
return "Error: No OAuth verifier provided"
|
150 |
+
|
151 |
+
auth = tweepy.OAuth1UserHandler(TWITTER_CLIENT_ID, TWITTER_CLIENT_SECRET)
|
152 |
+
auth.request_token = request_token
|
153 |
+
|
154 |
+
try:
|
155 |
+
auth.get_access_token(verifier)
|
156 |
+
session['twitter_access_token'] = auth.access_token
|
157 |
+
print("@@@@@@@@@@@@@@",auth.access_token)
|
158 |
+
|
159 |
+
access_token = auth.access_token
|
160 |
+
|
161 |
+
# Path to your existing JSON file
|
162 |
+
json_file_path = "access.json"
|
163 |
+
|
164 |
+
# Load the existing data
|
165 |
+
try:
|
166 |
+
with open(json_file_path, "r") as f:
|
167 |
+
data = json.load(f)
|
168 |
+
except (FileNotFoundError, json.JSONDecodeError):
|
169 |
+
data = {}
|
170 |
+
# Store or update the access_token
|
171 |
+
data["access_token"] = access_token
|
172 |
+
|
173 |
+
# Write it back to the file
|
174 |
+
with open(json_file_path, "w") as f:
|
175 |
+
json.dump(data, f, indent=4)
|
176 |
+
|
177 |
+
|
178 |
+
session['twitter_access_token_secret'] = auth.access_token_secret
|
179 |
+
|
180 |
+
#Set the access tokens to the auth handler
|
181 |
+
auth.set_access_token(auth.access_token, auth.access_token_secret)
|
182 |
+
api = tweepy.API(auth)
|
183 |
+
|
184 |
+
#Get user info
|
185 |
+
user = api.verify_credentials()
|
186 |
+
if user:
|
187 |
+
session['twitter_name'] = user.name # Full display name (e.g., "John Doe")
|
188 |
+
session['twitter_username'] = user.screen_name # Handle (e.g., "johndoe")
|
189 |
+
|
190 |
+
session.pop('connect_all', None)
|
191 |
+
return redirect(url_for('home'))
|
192 |
+
|
193 |
+
except tweepy.TweepyException as e:
|
194 |
+
return f"Twitter authorization failed: {e}"
|
195 |
+
|
196 |
+
|
197 |
+
|
198 |
+
@app.route('/disconnect/<platform>')
|
199 |
+
def disconnect(platform):
|
200 |
+
if platform == 'linkedin':
|
201 |
+
|
202 |
+
print("access",session["linkedin_access_token"])
|
203 |
+
session.pop('linkedin_access_token', None)
|
204 |
+
# print("sssss",session["linkedin_access_token"])
|
205 |
+
|
206 |
+
# session.pop('linkedin_access_token', None)
|
207 |
+
session.pop('linkedin_id', None)
|
208 |
+
elif platform == 'twitter':
|
209 |
+
session.pop('twitter_access_token', None)
|
210 |
+
session.pop('twitter_access_token_secret', None)
|
211 |
+
return redirect(url_for('home'))
|
212 |
+
|
213 |
+
@app.route('/post', methods=['GET', 'POST'])
|
214 |
+
def create_post():
|
215 |
+
if not (session.get('linkedin_access_token') or session.get('twitter_access_token')):
|
216 |
+
return redirect(url_for('home'))
|
217 |
+
|
218 |
+
if request.method == 'POST':
|
219 |
+
rss_urls = request.form.getlist('rss_urls')
|
220 |
+
posts_per_day = int(request.form['posts_per_day'])
|
221 |
+
frequency = request.form['frequency']
|
222 |
+
schedule_type = request.form['schedule_type']
|
223 |
+
first_post_time = datetime.strptime(request.form['first_post_time'], '%Y-%m-%dT%H:%M')
|
224 |
+
|
225 |
+
if schedule_type == 'daily':
|
226 |
+
total_posts = posts_per_day
|
227 |
+
elif schedule_type == 'weekly':
|
228 |
+
total_posts = posts_per_day * 7
|
229 |
+
else: # monthly
|
230 |
+
total_posts = posts_per_day * 30
|
231 |
+
|
232 |
+
all_entries = []
|
233 |
+
for rss_url in rss_urls:
|
234 |
+
feed = feedparser.parse(rss_url)
|
235 |
+
all_entries.extend(feed.entries)
|
236 |
+
|
237 |
+
selected_entries = random.sample(all_entries, min(total_posts, len(all_entries)))
|
238 |
+
|
239 |
+
# selected_entries = sorted(
|
240 |
+
# all_entries,
|
241 |
+
# key=lambda entry: entry.published_parsed,
|
242 |
+
# reverse=False
|
243 |
+
# )[:total_posts]
|
244 |
+
|
245 |
+
|
246 |
+
# selected_entries = sorted(
|
247 |
+
# all_entries,
|
248 |
+
# key=lambda entry: entry.published_parsed,
|
249 |
+
# reverse=False
|
250 |
+
# )[-3:]
|
251 |
+
|
252 |
+
|
253 |
+
generated_posts = {'linkedin': [], 'twitter': []}
|
254 |
+
if session.get('linkedin_access_token'):
|
255 |
+
for entry in selected_entries:
|
256 |
+
title = entry.title
|
257 |
+
description = entry.get('description', entry.get('summary', ''))
|
258 |
+
print("desc",description)
|
259 |
+
print(type(description))
|
260 |
+
image_url = extract_image_url(entry)
|
261 |
+
print("img_url",image_url)
|
262 |
+
if image_url == None:
|
263 |
+
print("here44444")
|
264 |
+
image_url = "https://static.libsyn.com/p/assets/0/2/1/3/0213c7d9616b570b16c3140a3186d450/LOGO_1400x1400.jpg"
|
265 |
+
transformed = agents.linkedin_transform(title, description)
|
266 |
+
|
267 |
+
text = f"{transformed['new_title']} {transformed['new_description']}"
|
268 |
+
generated_posts['linkedin'].append({
|
269 |
+
'text': text,
|
270 |
+
'image_url': image_url,
|
271 |
+
'platform': 'linkedin',
|
272 |
+
'access_token': session['linkedin_access_token'],
|
273 |
+
'linkedin_id': session['linkedin_id'],
|
274 |
+
'status': 'pending'
|
275 |
+
})
|
276 |
+
if session.get('twitter_access_token'):
|
277 |
+
for entry in selected_entries:
|
278 |
+
title = entry.title
|
279 |
+
description = entry.get('description', entry.get('summary', ''))
|
280 |
+
image_url = extract_image_url(entry)
|
281 |
+
print("desc",description)
|
282 |
+
print(type(description))
|
283 |
+
print("img_url",image_url)
|
284 |
+
if image_url == None:
|
285 |
+
print("here44444")
|
286 |
+
image_url = "https://static.libsyn.com/p/assets/0/2/1/3/0213c7d9616b570b16c3140a3186d450/LOGO_1400x1400.jpg"
|
287 |
+
|
288 |
+
|
289 |
+
transformed = agents.twitter_transform(title, description)
|
290 |
+
text = f"{transformed['new_title']} {transformed['new_description']}"
|
291 |
+
|
292 |
+
|
293 |
+
generated_posts['twitter'].append({
|
294 |
+
'text': text,
|
295 |
+
'image_url': image_url,
|
296 |
+
'platform': 'twitter',
|
297 |
+
'access_token': session['twitter_access_token'],
|
298 |
+
'access_token_secret': session['twitter_access_token_secret'],
|
299 |
+
'status': 'pending'
|
300 |
+
})
|
301 |
+
|
302 |
+
post_id = str(uuid.uuid4())
|
303 |
+
temp_posts[post_id] = {
|
304 |
+
'posts': generated_posts,
|
305 |
+
'first_post_time': first_post_time,
|
306 |
+
'frequency': int(frequency)
|
307 |
+
}
|
308 |
+
return redirect(url_for('review_posts', post_id=post_id))
|
309 |
+
|
310 |
+
return render_template('post.html')
|
311 |
+
|
312 |
+
@app.route('/review/<post_id>', methods=['GET', 'POST'])
|
313 |
+
def review_posts(post_id):
|
314 |
+
if post_id not in temp_posts:
|
315 |
+
return redirect(url_for('create_post'))
|
316 |
+
|
317 |
+
post_data = temp_posts[post_id]
|
318 |
+
all_posts = []
|
319 |
+
for platform_posts in post_data['posts'].values():
|
320 |
+
all_posts.extend(platform_posts)
|
321 |
+
|
322 |
+
if request.method == 'POST':
|
323 |
+
first_post_time = post_data['first_post_time']
|
324 |
+
frequency = post_data['frequency']
|
325 |
+
|
326 |
+
# Schedule posts separately for each platform
|
327 |
+
for platform, platform_posts in post_data['posts'].items():
|
328 |
+
for i, post in enumerate(platform_posts):
|
329 |
+
scheduled_time = first_post_time + timedelta(minutes=frequency * i)
|
330 |
+
post['scheduled_time'] = scheduled_time
|
331 |
+
posts.append(post)
|
332 |
+
if platform == 'linkedin':
|
333 |
+
scheduler.add_job(post_to_linkedin, 'date', run_date=scheduled_time, args=[post])
|
334 |
+
elif platform == 'twitter':
|
335 |
+
scheduler.add_job(post_to_twitter, 'date', run_date=scheduled_time, args=[post])
|
336 |
+
|
337 |
+
del temp_posts[post_id]
|
338 |
+
return redirect(url_for('scheduled_posts'))
|
339 |
+
|
340 |
+
return render_template('review.html',
|
341 |
+
posts=all_posts,
|
342 |
+
first_post_time=post_data['first_post_time'].isoformat(),
|
343 |
+
frequency=post_data['frequency'])
|
344 |
+
|
345 |
+
@app.route('/scheduled')
|
346 |
+
def scheduled_posts():
|
347 |
+
linkedin_posts = [p for p in posts if p['platform'] == 'linkedin' and p['status'] == 'pending']
|
348 |
+
twitter_posts = [p for p in posts if p['platform'] == 'twitter' and p['status'] == 'pending']
|
349 |
+
return render_template('scheduled.html', linkedin_posts=linkedin_posts, twitter_posts=twitter_posts)
|
350 |
+
|
351 |
+
|
352 |
+
|
353 |
+
|
354 |
+
|
355 |
+
def scheduled_task():
|
356 |
+
print(f"Scheduled task ran at: {datetime.now()}")
|
357 |
+
print("eeeeeewwwww",session.get('twitter_access_token'))
|
358 |
+
try:
|
359 |
+
|
360 |
+
json_file_path = 'access.json'
|
361 |
+
if os.path.exists(json_file_path) and os.path.getsize(json_file_path) > 0:
|
362 |
+
# Load existing JSON data
|
363 |
+
with open(json_file_path, "r") as f:
|
364 |
+
data = json.load(f)
|
365 |
+
|
366 |
+
# Save or update the access token
|
367 |
+
access_token = data["access_token"]
|
368 |
+
|
369 |
+
print("Access token saved.")
|
370 |
+
|
371 |
+
|
372 |
+
|
373 |
+
rss_url = "https://feeds.libsyn.com/123267/rss"
|
374 |
+
feed = feedparser.parse(rss_url)
|
375 |
+
print("Podcast Title:", feed.feed.title)
|
376 |
+
print("Podcast Link:", feed.feed.link)
|
377 |
+
print("Description:", feed.feed.get("description", "No description available."))
|
378 |
+
|
379 |
+
# Print latest episode info
|
380 |
+
print("\nLatest Episode:")
|
381 |
+
latest = feed.entries[0]
|
382 |
+
print("Title:", latest.title)
|
383 |
+
print("Published:", latest.published)
|
384 |
+
print("Link:", latest.link)
|
385 |
+
print("Description:", latest.description)
|
386 |
+
|
387 |
+
|
388 |
+
|
389 |
+
else:
|
390 |
+
print("login to linkedin")
|
391 |
+
|
392 |
+
|
393 |
+
except:
|
394 |
+
print("not")
|
395 |
+
|
396 |
+
@app.route('/ping-api')
|
397 |
+
def ping_api():
|
398 |
+
print("📡 Ping API endpoint hit!")
|
399 |
+
json_file_path = 'access.json'
|
400 |
+
if os.path.exists(json_file_path) and os.path.getsize(json_file_path) > 0:
|
401 |
+
# Load existing JSON data
|
402 |
+
with open(json_file_path, "r") as f:
|
403 |
+
data = json.load(f)
|
404 |
+
|
405 |
+
# Save or update the access token
|
406 |
+
access_token = data["access_token"]
|
407 |
+
|
408 |
+
print("Access token saved.")
|
409 |
+
|
410 |
+
|
411 |
+
|
412 |
+
rss_url = "https://feeds.libsyn.com/123267/rss"
|
413 |
+
feed = feedparser.parse(rss_url)
|
414 |
+
print("Podcast Title:", feed.feed.title)
|
415 |
+
print("Podcast Link:", feed.feed.link)
|
416 |
+
print("Description:", feed.feed.get("description", "No description available."))
|
417 |
+
|
418 |
+
# Print latest episode info
|
419 |
+
print("\nLatest Episode:")
|
420 |
+
latest = feed.entries[0]
|
421 |
+
print("Title:", latest.title)
|
422 |
+
print("Published:", latest.published)
|
423 |
+
|
424 |
+
if latest.published:
|
425 |
+
json_file_path = "latest.json"
|
426 |
+
|
427 |
+
# Load the existing data
|
428 |
+
try:
|
429 |
+
with open(json_file_path, "r") as f:
|
430 |
+
data = json.load(f)
|
431 |
+
except (FileNotFoundError, json.JSONDecodeError):
|
432 |
+
data = {}
|
433 |
+
# Store or update the access_token
|
434 |
+
data["access_token"] = access_token
|
435 |
+
|
436 |
+
# Write it back to the file
|
437 |
+
with open(json_file_path, "w") as f:
|
438 |
+
json.dump(data, f, indent=4)
|
439 |
+
print("Link:", latest.link)
|
440 |
+
print("Description:", latest.description)
|
441 |
+
|
442 |
+
|
443 |
+
|
444 |
+
else:
|
445 |
+
print("login to linkedin")
|
446 |
+
s = {
|
447 |
+
'na':'yes'
|
448 |
+
}
|
449 |
+
return s
|
450 |
+
|
451 |
+
def scheduled_task():
|
452 |
+
try:
|
453 |
+
response = requests.get("http://127.0.0.1:5000/ping-api")
|
454 |
+
print(f"[{datetime.now()}] Status: {response.status_code}, Response: {response.text}")
|
455 |
+
except requests.exceptions.RequestException as e:
|
456 |
+
print(f"[{datetime.now()}] Failed to call API: {e}")
|
457 |
+
|
458 |
+
|
459 |
+
# ✅ Setup scheduler
|
460 |
+
scheduler = BackgroundScheduler()
|
461 |
+
scheduler.add_job(func=scheduled_task, trigger="interval", seconds=10)
|
462 |
+
scheduler.start()
|
463 |
+
|
464 |
+
# ✅ Clean up scheduler on shutdown
|
465 |
+
atexit.register(lambda: scheduler.shutdown())
|
466 |
+
|
467 |
+
|
468 |
+
|
469 |
+
|
470 |
+
if __name__ == '__main__':
|
471 |
+
port = int(os.environ.get("PORT", 5000)) # Get the port from Render
|
472 |
+
app.run(debug=True, host='0.0.0.0', port=port)
|
helpers.py
ADDED
@@ -0,0 +1,193 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import re
|
2 |
+
from io import BytesIO
|
3 |
+
import requests
|
4 |
+
import tweepy
|
5 |
+
|
6 |
+
import os
|
7 |
+
from dotenv import load_dotenv
|
8 |
+
load_dotenv()
|
9 |
+
|
10 |
+
LINKEDIN_CLIENT_ID = os.getenv("LINKEDIN_CLIENT_ID")
|
11 |
+
LINKEDIN_CLIENT_SECRET = os.getenv("LINKEDIN_CLIENT_SECRET")
|
12 |
+
TWITTER_CLIENT_ID = os.getenv("TWITTER_CLIENT_ID")
|
13 |
+
TWITTER_CLIENT_SECRET = os.getenv("TWITTER_CLIENT_SECRET")
|
14 |
+
|
15 |
+
def extract_image_url(entry):
|
16 |
+
"""Extract an image URL from an RSS feed entry."""
|
17 |
+
for enclosure in entry.get('enclosures', []):
|
18 |
+
if enclosure.get('type', '').startswith('image/'):
|
19 |
+
return enclosure.get('url')
|
20 |
+
for media in entry.get('media_content', []):
|
21 |
+
if media.get('type', '').startswith('image/'):
|
22 |
+
return media.get('url')
|
23 |
+
for thumbnail in entry.get('media_thumbnail', []):
|
24 |
+
if thumbnail.get('url'):
|
25 |
+
return thumbnail.get('url')
|
26 |
+
if 'image' in entry:
|
27 |
+
image = entry['image']
|
28 |
+
if isinstance(image, dict) and 'url' in image:
|
29 |
+
return image['url']
|
30 |
+
elif isinstance(image, list):
|
31 |
+
for img in image:
|
32 |
+
if 'url' in img:
|
33 |
+
return img['url']
|
34 |
+
if 'itunes_image' in entry:
|
35 |
+
return entry['itunes_image'].get('href')
|
36 |
+
for field in ['description', 'summary', 'content']:
|
37 |
+
if field in entry:
|
38 |
+
content = entry[field]
|
39 |
+
if isinstance(content, list):
|
40 |
+
content = content[0].get('value', '')
|
41 |
+
elif isinstance(content, dict):
|
42 |
+
content = content.get('value', '')
|
43 |
+
else:
|
44 |
+
content = str(content)
|
45 |
+
match = re.search(r'<img[^>]+src=["\'](.*?)["\']', content, re.I)
|
46 |
+
if match:
|
47 |
+
return match.group(1)
|
48 |
+
return None
|
49 |
+
|
50 |
+
def post_to_linkedin(post):
|
51 |
+
"""Post content to LinkedIn with optional image."""
|
52 |
+
if post['status'] not in ['pending', 'posting']:
|
53 |
+
return
|
54 |
+
access_token = post['access_token']
|
55 |
+
|
56 |
+
|
57 |
+
print("linkedin_access_token",access_token)
|
58 |
+
linkedin_id = post['linkedin_id']
|
59 |
+
image_url = post.get('image_url')
|
60 |
+
headers = {
|
61 |
+
'Authorization': f'Bearer {access_token}',
|
62 |
+
'Content-Type': 'application/json',
|
63 |
+
}
|
64 |
+
if image_url:
|
65 |
+
response = requests.get(image_url, timeout=10)
|
66 |
+
if response.status_code == 200:
|
67 |
+
image_content = response.content
|
68 |
+
register_url = 'https://api.linkedin.com/v2/assets?action=registerUpload'
|
69 |
+
register_body = {
|
70 |
+
'registerUploadRequest': {
|
71 |
+
'recipes': ['urn:li:digitalmediaRecipe:feedshare-image'],
|
72 |
+
'owner': f'urn:li:person:{linkedin_id}',
|
73 |
+
'serviceRelationships': [
|
74 |
+
{'relationshipType': 'OWNER', 'identifier': 'urn:li:userGeneratedContent'}
|
75 |
+
]
|
76 |
+
}
|
77 |
+
}
|
78 |
+
register_response = requests.post(register_url, headers=headers, json=register_body)
|
79 |
+
if register_response.status_code == 200:
|
80 |
+
upload_data = register_response.json()['value']
|
81 |
+
upload_url = upload_data['uploadMechanism']['com.linkedin.digitalmedia.uploading.MediaUploadHttpRequest']['uploadUrl']
|
82 |
+
asset = upload_data['asset']
|
83 |
+
upload_headers = {'Authorization': f'Bearer {access_token}'}
|
84 |
+
upload_response = requests.put(upload_url, headers=upload_headers, data=image_content)
|
85 |
+
if upload_response.status_code == 201:
|
86 |
+
api_url = 'https://api.linkedin.com/v2/ugcPosts'
|
87 |
+
post_body = {
|
88 |
+
'author': f'urn:li:person:{linkedin_id}',
|
89 |
+
'lifecycleState': 'PUBLISHED',
|
90 |
+
'specificContent': {
|
91 |
+
'com.linkedin.ugc.ShareContent': {
|
92 |
+
'shareCommentary': {'text': post['text']},
|
93 |
+
'shareMediaCategory': 'IMAGE',
|
94 |
+
'media': [{'status': 'READY', 'media': asset}]
|
95 |
+
}
|
96 |
+
},
|
97 |
+
'visibility': {'com.linkedin.ugc.MemberNetworkVisibility': 'PUBLIC'}
|
98 |
+
}
|
99 |
+
response = requests.post(api_url, headers=headers, json=post_body)
|
100 |
+
post['status'] = 'posted' if response.status_code == 201 else 'failed'
|
101 |
+
print(f"LinkedIn post attempt: {response.status_code} - {response.text}")
|
102 |
+
else:
|
103 |
+
print(f"Image upload failed: {upload_response.status_code}")
|
104 |
+
else:
|
105 |
+
print(f"Upload registration failed: {register_response.status_code}")
|
106 |
+
else:
|
107 |
+
print(f"Image download failed: {response.status_code}")
|
108 |
+
if post['status'] != 'posted':
|
109 |
+
api_url = 'https://api.linkedin.com/v2/ugcPosts'
|
110 |
+
post_body = {
|
111 |
+
'author': f'urn:li:person:{linkedin_id}',
|
112 |
+
'lifecycleState': 'PUBLISHED',
|
113 |
+
'specificContent': {
|
114 |
+
'com.linkedin.ugc.ShareContent': {
|
115 |
+
'shareCommentary': {'text': post['text']},
|
116 |
+
'shareMediaCategory': 'NONE'
|
117 |
+
}
|
118 |
+
},
|
119 |
+
'visibility': {'com.linkedin.ugc.MemberNetworkVisibility': 'PUBLIC'}
|
120 |
+
}
|
121 |
+
response = requests.post(api_url, headers=headers, json=post_body)
|
122 |
+
post['status'] = 'posted' if response.status_code == 201 else 'failed'
|
123 |
+
print(f"LinkedIn text-only post: {response.status_code} - {response.text}")
|
124 |
+
else:
|
125 |
+
api_url = 'https://api.linkedin.com/v2/ugcPosts'
|
126 |
+
post_body = {
|
127 |
+
'author': f'urn:li:person:{linkedin_id}',
|
128 |
+
'lifecycleState': 'PUBLISHED',
|
129 |
+
'specificContent': {
|
130 |
+
'com.linkedin.ugc.ShareContent': {
|
131 |
+
'shareCommentary': {'text': post['text']},
|
132 |
+
'shareMediaCategory': 'NONE'
|
133 |
+
}
|
134 |
+
},
|
135 |
+
'visibility': {'com.linkedin.ugc.MemberNetworkVisibility': 'PUBLIC'}
|
136 |
+
}
|
137 |
+
response = requests.post(api_url, headers=headers, json=post_body)
|
138 |
+
post['status'] = 'posted' if response.status_code == 201 else 'failed'
|
139 |
+
print(f"LinkedIn post attempt: {response.status_code} - {response.text}")
|
140 |
+
|
141 |
+
def post_to_twitter(post):
|
142 |
+
"""Post content to Twitter with optional image."""
|
143 |
+
if post['status'] not in ['pending', 'posting']:
|
144 |
+
return
|
145 |
+
client = tweepy.Client(
|
146 |
+
consumer_key=TWITTER_CLIENT_ID,
|
147 |
+
consumer_secret=TWITTER_CLIENT_SECRET,
|
148 |
+
access_token=post['access_token'],
|
149 |
+
access_token_secret=post['access_token_secret']
|
150 |
+
)
|
151 |
+
print("access_token_secret",client.access_token_secret)
|
152 |
+
image_url = post.get('image_url')
|
153 |
+
if image_url:
|
154 |
+
response = requests.get(image_url, timeout=10)
|
155 |
+
if response.status_code == 200:
|
156 |
+
image_content = BytesIO(response.content)
|
157 |
+
try:
|
158 |
+
api = tweepy.API(tweepy.OAuth1UserHandler(
|
159 |
+
TWITTER_CLIENT_ID, TWITTER_CLIENT_SECRET,
|
160 |
+
post['access_token'], post['access_token_secret']
|
161 |
+
))
|
162 |
+
media = api.media_upload(filename='image', file=image_content)
|
163 |
+
client.create_tweet(text=post['text'], media_ids=[media.media_id])
|
164 |
+
post['status'] = 'posted'
|
165 |
+
print("Twitter post with image successful")
|
166 |
+
except tweepy.TweepyException as e:
|
167 |
+
print(f"Twitter image post error: {e}")
|
168 |
+
try:
|
169 |
+
client.create_tweet(text=post['text'])
|
170 |
+
post['status'] = 'posted'
|
171 |
+
print("Twitter text-only post successful")
|
172 |
+
except tweepy.TweepyException as e:
|
173 |
+
post['status'] = 'failed'
|
174 |
+
print(f"Twitter text-only error: {e}")
|
175 |
+
except Exception as e:
|
176 |
+
print(f"Media upload error: {e}")
|
177 |
+
else:
|
178 |
+
print(f"Image download failed: {response.status_code}")
|
179 |
+
try:
|
180 |
+
client.create_tweet(text=post['text'])
|
181 |
+
post['status'] = 'posted'
|
182 |
+
print("Twitter text-only post successful")
|
183 |
+
except tweepy.TweepyException as e:
|
184 |
+
post['status'] = 'failed'
|
185 |
+
print(f"Twitter text-only error: {e}")
|
186 |
+
else:
|
187 |
+
try:
|
188 |
+
client.create_tweet(text=post['text'])
|
189 |
+
post['status'] = 'posted'
|
190 |
+
print("Twitter post successful")
|
191 |
+
except tweepy.TweepyException as e:
|
192 |
+
post['status'] = 'failed'
|
193 |
+
print(f"Twitter error: {e}")
|
requirements.txt
ADDED
@@ -0,0 +1,10 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
flask
|
2 |
+
apscheduler
|
3 |
+
requests
|
4 |
+
tweepy
|
5 |
+
feedparser
|
6 |
+
langchain_google_genai
|
7 |
+
langchain_core
|
8 |
+
flask-session
|
9 |
+
langchain
|
10 |
+
python-dotenv
|
test2.py
ADDED
@@ -0,0 +1,306 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
from flask import Flask, redirect, url_for, request, render_template, session
|
2 |
+
from apscheduler.schedulers.background import BackgroundScheduler
|
3 |
+
import requests
|
4 |
+
from datetime import datetime, timedelta
|
5 |
+
import tweepy
|
6 |
+
from agents import SocialMediaAgents # Assuming this is your agents.py file
|
7 |
+
import feedparser
|
8 |
+
from helpers import post_to_linkedin, post_to_twitter, extract_image_url
|
9 |
+
import random
|
10 |
+
import uuid
|
11 |
+
from dotenv import load_dotenv
|
12 |
+
import os
|
13 |
+
|
14 |
+
load_dotenv()
|
15 |
+
|
16 |
+
ngrok_link = os.getenv("Ngrok_Link")
|
17 |
+
|
18 |
+
app = Flask(__name__)
|
19 |
+
app.secret_key = '12345678765' # Replace with a secure key
|
20 |
+
|
21 |
+
scheduler = BackgroundScheduler()
|
22 |
+
scheduler.start()
|
23 |
+
|
24 |
+
api_key = os.getenv("Gemini_key")
|
25 |
+
|
26 |
+
agents = SocialMediaAgents(api_key)
|
27 |
+
|
28 |
+
LINKEDIN_CLIENT_ID = os.getenv("LINKEDIN_CLIENT_ID")
|
29 |
+
LINKEDIN_CLIENT_SECRET = os.getenv("LINKEDIN_CLIENT_SECRET")
|
30 |
+
TWITTER_CLIENT_ID = os.getenv("TWITTER_CLIENT_ID")
|
31 |
+
TWITTER_CLIENT_SECRET = os.getenv("TWITTER_CLIENT_SECRET")
|
32 |
+
|
33 |
+
posts = []
|
34 |
+
temp_posts = {}
|
35 |
+
|
36 |
+
@app.route('/')
|
37 |
+
def home():
|
38 |
+
connected_platforms = {
|
39 |
+
'linkedin': 'linkedin_access_token' in session and 'linkedin_id' in session,
|
40 |
+
'twitter': 'twitter_access_token' in session and 'twitter_access_token_secret' in session
|
41 |
+
}
|
42 |
+
|
43 |
+
name ={
|
44 |
+
'name':session.get('linkedin_name'),
|
45 |
+
'tw_name':session.get('twitter_name')
|
46 |
+
}
|
47 |
+
|
48 |
+
|
49 |
+
return render_template('home.html', connected_platforms=connected_platforms,name=name)
|
50 |
+
|
51 |
+
@app.route('/connect_all')
|
52 |
+
def connect_all():
|
53 |
+
session['connect_all'] = True
|
54 |
+
return redirect(url_for('linkedin_auth'))
|
55 |
+
|
56 |
+
@app.route('/linkedin/auth')
|
57 |
+
def linkedin_auth():
|
58 |
+
redirect_uri = f'{ngrok_link}/linkedin/callback'
|
59 |
+
scope = 'openid profile w_member_social'
|
60 |
+
auth_url = (
|
61 |
+
f'https://www.linkedin.com/oauth/v2/authorization?'
|
62 |
+
f'response_type=code&client_id={LINKEDIN_CLIENT_ID}&redirect_uri={redirect_uri}&'
|
63 |
+
f'scope={scope}&state=randomstring'
|
64 |
+
)
|
65 |
+
|
66 |
+
print("auth------------",auth_url)
|
67 |
+
return redirect(auth_url)
|
68 |
+
|
69 |
+
@app.route('/linkedin/callback')
|
70 |
+
def linkedin_callback():
|
71 |
+
code = request.args.get('code')
|
72 |
+
if not code:
|
73 |
+
return "Error: No authorization code provided"
|
74 |
+
|
75 |
+
print("code11111111",code)
|
76 |
+
token_url = 'https://www.linkedin.com/oauth/v2/accessToken'
|
77 |
+
data = {
|
78 |
+
'grant_type': 'authorization_code',
|
79 |
+
'code': code,
|
80 |
+
'redirect_uri': f'{ngrok_link}/linkedin/callback',
|
81 |
+
'client_id': LINKEDIN_CLIENT_ID,
|
82 |
+
'client_secret': LINKEDIN_CLIENT_SECRET
|
83 |
+
}
|
84 |
+
response = requests.post(token_url, data=data)
|
85 |
+
if response.status_code != 200:
|
86 |
+
return "Error: Could not get LinkedIn access token"
|
87 |
+
token_data = response.json()
|
88 |
+
session['linkedin_access_token'] = token_data.get('access_token')
|
89 |
+
profile_url = 'https://api.linkedin.com/v2/userinfo'
|
90 |
+
headers = {'Authorization': f'Bearer {session["linkedin_access_token"]}'}
|
91 |
+
profile_response = requests.get(profile_url, headers=headers)
|
92 |
+
if profile_response.status_code != 200:
|
93 |
+
return "Error: Could not fetch LinkedIn profile"
|
94 |
+
user_info = profile_response.json()
|
95 |
+
|
96 |
+
session['linkedin_name'] = user_info['name']
|
97 |
+
session['linkedin_id'] = user_info.get('sub')
|
98 |
+
|
99 |
+
if session.get('connect_all') and 'twitter_access_token' not in session:
|
100 |
+
return redirect(url_for('twitter_auth'))
|
101 |
+
return redirect(url_for('home'))
|
102 |
+
|
103 |
+
@app.route('/twitter/auth')
|
104 |
+
def twitter_auth():
|
105 |
+
auth = tweepy.OAuth1UserHandler(TWITTER_CLIENT_ID, TWITTER_CLIENT_SECRET, f'{ngrok_link}/twitter/callback')
|
106 |
+
try:
|
107 |
+
redirect_url = auth.get_authorization_url()
|
108 |
+
session['request_token'] = auth.request_token
|
109 |
+
return redirect(redirect_url)
|
110 |
+
except tweepy.TweepyException as e:
|
111 |
+
return f"Error starting Twitter auth: {e}"
|
112 |
+
|
113 |
+
@app.route('/twitter/callback')
|
114 |
+
def twitter_callback():
|
115 |
+
request_token = session.pop('request_token', None)
|
116 |
+
if not request_token:
|
117 |
+
return "Error: Request token not found in session. <a href='/twitter/auth'>Please try logging in again</a>."
|
118 |
+
verifier = request.args.get('oauth_verifier')
|
119 |
+
if not verifier:
|
120 |
+
return "Error: No OAuth verifier provided"
|
121 |
+
auth = tweepy.OAuth1UserHandler(TWITTER_CLIENT_ID, TWITTER_CLIENT_SECRET)
|
122 |
+
auth.request_token = request_token
|
123 |
+
try:
|
124 |
+
auth.get_access_token(verifier)
|
125 |
+
session['twitter_access_token'] = auth.access_token
|
126 |
+
session['twitter_access_token_secret'] = auth.access_token_secret
|
127 |
+
|
128 |
+
print("twitter_session = 1",session.get('twitter_access_token'))
|
129 |
+
auth.set_access_token(auth.access_token, auth.access_token_secret)
|
130 |
+
api = tweepy.API(auth)
|
131 |
+
|
132 |
+
|
133 |
+
user = api.verify_credentials()
|
134 |
+
if user:
|
135 |
+
session['twitter_name'] = user.name
|
136 |
+
# session['twitter_username'] = user.screen_name
|
137 |
+
|
138 |
+
|
139 |
+
session.pop('connect_all', None)
|
140 |
+
return redirect(url_for('home'))
|
141 |
+
except tweepy.TweepyException as e:
|
142 |
+
return f"Twitter authorization failed: {e}"
|
143 |
+
|
144 |
+
@app.route('/disconnect/<platform>')
|
145 |
+
def disconnect(platform):
|
146 |
+
if platform == 'linkedin':
|
147 |
+
session.pop('linkedin_access_token', None)
|
148 |
+
session.pop('linkedin_id', None)
|
149 |
+
elif platform == 'twitter':
|
150 |
+
session.pop('twitter_access_token', None)
|
151 |
+
session.pop('twitter_access_token_secret', None)
|
152 |
+
return redirect(url_for('home'))
|
153 |
+
|
154 |
+
@app.route('/post', methods=['GET', 'POST'])
|
155 |
+
def create_post():
|
156 |
+
if not (session.get('linkedin_access_token') or session.get('twitter_access_token')):
|
157 |
+
return redirect(url_for('home'))
|
158 |
+
|
159 |
+
if request.method == 'POST':
|
160 |
+
rss_urls = request.form.getlist('rss_urls')
|
161 |
+
posts_per_day = int(request.form['posts_per_day'])
|
162 |
+
frequency = request.form['frequency']
|
163 |
+
schedule_type = request.form['schedule_type']
|
164 |
+
first_post_time = datetime.strptime(request.form['first_post_time'], '%Y-%m-%dT%H:%M')
|
165 |
+
|
166 |
+
if schedule_type == 'daily':
|
167 |
+
total_posts = posts_per_day
|
168 |
+
elif schedule_type == 'weekly':
|
169 |
+
total_posts = posts_per_day * 7
|
170 |
+
else: # monthly
|
171 |
+
total_posts = posts_per_day * 30
|
172 |
+
|
173 |
+
all_entries = []
|
174 |
+
for rss_url in rss_urls:
|
175 |
+
feed = feedparser.parse(rss_url)
|
176 |
+
all_entries.extend(feed.entries)
|
177 |
+
|
178 |
+
selected_entries = random.sample(all_entries, min(total_posts, len(all_entries)))
|
179 |
+
|
180 |
+
# selected_entries = sorted(
|
181 |
+
# all_entries,
|
182 |
+
# key=lambda entry: entry.published_parsed,
|
183 |
+
# reverse=False
|
184 |
+
# )[:total_posts]
|
185 |
+
|
186 |
+
|
187 |
+
# selected_entries = sorted(
|
188 |
+
# all_entries,
|
189 |
+
# key=lambda entry: entry.published_parsed,
|
190 |
+
# reverse=False
|
191 |
+
# )[-3:]
|
192 |
+
|
193 |
+
|
194 |
+
generated_posts = {'linkedin': [], 'twitter': []}
|
195 |
+
if session.get('linkedin_access_token'):
|
196 |
+
for entry in selected_entries:
|
197 |
+
title = entry.title
|
198 |
+
description = entry.get('description', entry.get('summary', ''))
|
199 |
+
print("desc",description)
|
200 |
+
print(type(description))
|
201 |
+
image_url = extract_image_url(entry)
|
202 |
+
print("img_url",image_url)
|
203 |
+
link = entry.get('link', '')
|
204 |
+
if image_url == None:
|
205 |
+
print("here44444")
|
206 |
+
image_url = "https://static.libsyn.com/p/assets/0/2/1/3/0213c7d9616b570b16c3140a3186d450/LOGO_1400x1400.jpg"
|
207 |
+
transformed = agents.linkedin_transform(title, description,link)
|
208 |
+
|
209 |
+
text = f"{transformed['new_title']} {transformed['new_description']}"
|
210 |
+
generated_posts['linkedin'].append({
|
211 |
+
'text': text,
|
212 |
+
'image_url': image_url,
|
213 |
+
'platform': 'linkedin',
|
214 |
+
'access_token': session['linkedin_access_token'],
|
215 |
+
'linkedin_id': session['linkedin_id'],
|
216 |
+
'status': 'pending'
|
217 |
+
})
|
218 |
+
if session.get('twitter_access_token'):
|
219 |
+
print("twitter_session = 2",session.get('twitter_access_token'))
|
220 |
+
for entry in selected_entries:
|
221 |
+
title = entry.title
|
222 |
+
description = entry.get('description', entry.get('summary', ''))
|
223 |
+
image_url = extract_image_url(entry)
|
224 |
+
print("desc",description)
|
225 |
+
print(type(description))
|
226 |
+
print("img_url",image_url)
|
227 |
+
link = entry.get('link', '')
|
228 |
+
if image_url == None:
|
229 |
+
print("here44444")
|
230 |
+
image_url = "https://static.libsyn.com/p/assets/0/2/1/3/0213c7d9616b570b16c3140a3186d450/LOGO_1400x1400.jpg"
|
231 |
+
|
232 |
+
|
233 |
+
transformed = agents.twitter_transform(title, description,link)
|
234 |
+
text = f"{transformed['new_title']} {transformed['new_description']}"
|
235 |
+
|
236 |
+
|
237 |
+
generated_posts['twitter'].append({
|
238 |
+
'text': text,
|
239 |
+
'image_url': image_url,
|
240 |
+
'platform': 'twitter',
|
241 |
+
'access_token': session['twitter_access_token'],
|
242 |
+
'access_token_secret': session['twitter_access_token_secret'],
|
243 |
+
'status': 'pending'
|
244 |
+
})
|
245 |
+
|
246 |
+
post_id = str(uuid.uuid4())
|
247 |
+
temp_posts[post_id] = {
|
248 |
+
'posts': generated_posts,
|
249 |
+
'first_post_time': first_post_time,
|
250 |
+
'frequency': int(frequency)
|
251 |
+
}
|
252 |
+
return redirect(url_for('review_posts', post_id=post_id))
|
253 |
+
|
254 |
+
return render_template('post.html')
|
255 |
+
|
256 |
+
@app.route('/review/<post_id>', methods=['GET', 'POST'])
|
257 |
+
def review_posts(post_id):
|
258 |
+
if post_id not in temp_posts:
|
259 |
+
return redirect(url_for('create_post'))
|
260 |
+
|
261 |
+
now = datetime.now()
|
262 |
+
|
263 |
+
current_time = now.strftime("%H:%M:%S")
|
264 |
+
print("Current Time =", current_time)
|
265 |
+
|
266 |
+
post_data = temp_posts[post_id]
|
267 |
+
all_posts = []
|
268 |
+
for platform_posts in post_data['posts'].values():
|
269 |
+
all_posts.extend(platform_posts)
|
270 |
+
|
271 |
+
if request.method == 'POST':
|
272 |
+
first_post_time = post_data['first_post_time']
|
273 |
+
frequency = post_data['frequency']
|
274 |
+
|
275 |
+
# Schedule posts separately for each platform
|
276 |
+
for platform, platform_posts in post_data['posts'].items():
|
277 |
+
for i, post in enumerate(platform_posts):
|
278 |
+
scheduled_time = first_post_time + timedelta(minutes=frequency * i)
|
279 |
+
post['scheduled_time'] = scheduled_time
|
280 |
+
posts.append(post)
|
281 |
+
if platform == 'linkedin':
|
282 |
+
scheduler.add_job(post_to_linkedin, 'date', run_date=scheduled_time, args=[post])
|
283 |
+
elif platform == 'twitter':
|
284 |
+
|
285 |
+
print("pooooooooossssssssssssttttttt",post)
|
286 |
+
scheduler.add_job(post_to_twitter, 'date', run_date=scheduled_time, args=[post])
|
287 |
+
now = datetime.now()
|
288 |
+
current_time = now.strftime("%H:%M:%S")
|
289 |
+
print("end Time =", current_time)
|
290 |
+
del temp_posts[post_id]
|
291 |
+
return redirect(url_for('scheduled_posts'))
|
292 |
+
|
293 |
+
return render_template('review.html',
|
294 |
+
posts=all_posts,
|
295 |
+
first_post_time=post_data['first_post_time'].isoformat(),
|
296 |
+
frequency=post_data['frequency'])
|
297 |
+
|
298 |
+
@app.route('/scheduled')
|
299 |
+
def scheduled_posts():
|
300 |
+
linkedin_posts = [p for p in posts if p['platform'] == 'linkedin' and p['status'] == 'pending']
|
301 |
+
twitter_posts = [p for p in posts if p['platform'] == 'twitter' and p['status'] == 'pending']
|
302 |
+
return render_template('scheduled.html', linkedin_posts=linkedin_posts, twitter_posts=twitter_posts)
|
303 |
+
|
304 |
+
if __name__ == '__main__':
|
305 |
+
port = int(os.environ.get("PORT", 5000)) # Get the port from Render
|
306 |
+
app.run(debug=True, host='0.0.0.0', port=port)
|