Spaces:
Sleeping
Sleeping
Add analytics map and logo images; enhance LinkedIn posting function for better user feedback and error handling
Browse files
app.py
CHANGED
|
@@ -3,6 +3,7 @@ import requests
|
|
| 3 |
import json
|
| 4 |
import os
|
| 5 |
from dotenv import load_dotenv
|
|
|
|
| 6 |
|
| 7 |
# Load environment variables from .env file (for local development)
|
| 8 |
load_dotenv()
|
|
@@ -25,21 +26,29 @@ if missing_vars:
|
|
| 25 |
print(f"⚠️ WARNING: Missing environment variables: {', '.join(missing_vars)}")
|
| 26 |
print("Please check your Hugging Face Spaces Secrets configuration")
|
| 27 |
|
|
|
|
| 28 |
def post_to_linkedin(name, job_post):
|
| 29 |
-
"""Post to LinkedIn via their API"""
|
|
|
|
|
|
|
|
|
|
| 30 |
|
| 31 |
-
# Check for missing environment variables
|
| 32 |
if not organisationNumber or not token or not postApiUrl:
|
| 33 |
-
|
|
|
|
| 34 |
|
|
|
|
| 35 |
if not name.strip() or not job_post.strip():
|
| 36 |
-
|
|
|
|
| 37 |
|
|
|
|
| 38 |
headers_dict = {
|
| 39 |
'Authorization': f'Bearer {token}',
|
| 40 |
'Content-Type': 'application/json',
|
| 41 |
'X-Restli-Protocol-Version': '2.0.0',
|
| 42 |
-
'LinkedIn-Version': '202510'
|
| 43 |
}
|
| 44 |
|
| 45 |
data = {
|
|
@@ -55,68 +64,264 @@ def post_to_linkedin(name, job_post):
|
|
| 55 |
"isReshareDisabledByAuthor": False
|
| 56 |
}
|
| 57 |
|
|
|
|
| 58 |
try:
|
| 59 |
-
|
|
|
|
| 60 |
response = requests.post(
|
| 61 |
postApiUrl,
|
| 62 |
headers=headers_dict,
|
| 63 |
json=data,
|
| 64 |
-
timeout=30
|
| 65 |
)
|
| 66 |
|
| 67 |
-
|
| 68 |
-
|
| 69 |
if response.status_code == 201:
|
| 70 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 71 |
else:
|
| 72 |
-
|
| 73 |
-
|
| 74 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 75 |
|
| 76 |
except requests.exceptions.Timeout:
|
| 77 |
-
|
| 78 |
except requests.exceptions.RequestException as e:
|
| 79 |
-
|
| 80 |
except Exception as e:
|
| 81 |
-
|
|
|
|
| 82 |
|
| 83 |
-
#
|
| 84 |
-
|
| 85 |
-
|
| 86 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 87 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 88 |
# Show configuration status
|
| 89 |
if missing_vars:
|
| 90 |
-
gr.
|
|
|
|
|
|
|
| 91 |
|
| 92 |
-
|
| 93 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 94 |
name_input = gr.Textbox(
|
| 95 |
-
label="Name",
|
| 96 |
-
placeholder="
|
| 97 |
lines=1
|
| 98 |
)
|
| 99 |
job_post_input = gr.Textbox(
|
| 100 |
-
label="Job Post",
|
| 101 |
-
placeholder="Enter job post details",
|
| 102 |
lines=10
|
| 103 |
)
|
| 104 |
-
|
| 105 |
-
|
| 106 |
-
|
| 107 |
-
|
| 108 |
-
|
| 109 |
-
|
| 110 |
-
|
| 111 |
-
|
| 112 |
-
|
| 113 |
-
|
| 114 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 115 |
submit_btn.click(
|
| 116 |
fn=post_to_linkedin,
|
| 117 |
inputs=[name_input, job_post_input],
|
| 118 |
-
outputs=[output, success_link]
|
|
|
|
| 119 |
)
|
| 120 |
|
| 121 |
if __name__ == "__main__":
|
| 122 |
-
demo.launch()
|
|
|
|
| 3 |
import json
|
| 4 |
import os
|
| 5 |
from dotenv import load_dotenv
|
| 6 |
+
import time
|
| 7 |
|
| 8 |
# Load environment variables from .env file (for local development)
|
| 9 |
load_dotenv()
|
|
|
|
| 26 |
print(f"⚠️ WARNING: Missing environment variables: {', '.join(missing_vars)}")
|
| 27 |
print("Please check your Hugging Face Spaces Secrets configuration")
|
| 28 |
|
| 29 |
+
|
| 30 |
def post_to_linkedin(name, job_post):
|
| 31 |
+
"""Post to LinkedIn via their API with improved UX feedback"""
|
| 32 |
+
|
| 33 |
+
# 1. Clear previous success link on new submission
|
| 34 |
+
yield "Verifying inputs...", gr.update(value="", visible=False)
|
| 35 |
|
| 36 |
+
# 2. Check for missing environment variables
|
| 37 |
if not organisationNumber or not token or not postApiUrl:
|
| 38 |
+
yield f"❌ Configuration Error: Missing environment variables. Please check Spaces Secrets.", gr.update(visible=False)
|
| 39 |
+
return
|
| 40 |
|
| 41 |
+
# 3. Validate user input
|
| 42 |
if not name.strip() or not job_post.strip():
|
| 43 |
+
yield "❌ Please fill in all fields (Name and Job Post).", gr.update(visible=False)
|
| 44 |
+
return
|
| 45 |
|
| 46 |
+
# 4. Prepare headers and data
|
| 47 |
headers_dict = {
|
| 48 |
'Authorization': f'Bearer {token}',
|
| 49 |
'Content-Type': 'application/json',
|
| 50 |
'X-Restli-Protocol-Version': '2.0.0',
|
| 51 |
+
'LinkedIn-Version': '202510' # Using a recent, static version
|
| 52 |
}
|
| 53 |
|
| 54 |
data = {
|
|
|
|
| 64 |
"isReshareDisabledByAuthor": False
|
| 65 |
}
|
| 66 |
|
| 67 |
+
# 5. Make the API POST request
|
| 68 |
try:
|
| 69 |
+
yield "🚀 Posting to LinkedIn...", gr.update(visible=False)
|
| 70 |
+
|
| 71 |
response = requests.post(
|
| 72 |
postApiUrl,
|
| 73 |
headers=headers_dict,
|
| 74 |
json=data,
|
| 75 |
+
timeout=30
|
| 76 |
)
|
| 77 |
|
| 78 |
+
# 6. Handle the POST response
|
|
|
|
| 79 |
if response.status_code == 201:
|
| 80 |
+
|
| 81 |
+
# --- REINTRODUCED COUNTDOWN START ---
|
| 82 |
+
for i in range(10, 0, -1):
|
| 83 |
+
yield f"✅ Post successful! Waiting for LinkedIn API update... ({i} seconds remaining)", gr.update(visible=False)
|
| 84 |
+
time.sleep(1)
|
| 85 |
+
# --- REINTRODUCED COUNTDOWN END ---
|
| 86 |
+
|
| 87 |
+
yield "✅ Post live! 🔍 Retrieving your post link...", gr.update(visible=False)
|
| 88 |
+
|
| 89 |
+
try:
|
| 90 |
+
# 7. Attempt to FETCH the post URL
|
| 91 |
+
post_api_url = "https://api.linkedin.com/rest/posts"
|
| 92 |
+
headers = {
|
| 93 |
+
"Authorization": f"Bearer {token}",
|
| 94 |
+
"LinkedIn-Version": "202510",
|
| 95 |
+
"X-Restli-Protocol-Version": "2.0.0"
|
| 96 |
+
}
|
| 97 |
+
params = {
|
| 98 |
+
"q": "author",
|
| 99 |
+
"author": f"urn:li:organization:{organisationNumber}",
|
| 100 |
+
"count": 1 # Get the single most recent post
|
| 101 |
+
}
|
| 102 |
+
|
| 103 |
+
fetch_response = requests.get(post_api_url, headers=headers, params=params, timeout=30)
|
| 104 |
+
|
| 105 |
+
if fetch_response.status_code == 200:
|
| 106 |
+
fetch_data = fetch_response.json()
|
| 107 |
+
if fetch_data.get("elements") and len(fetch_data["elements"]) > 0:
|
| 108 |
+
post_id_urn = fetch_data["elements"][0]["id"] # e.g., "urn:li:share:12345"
|
| 109 |
+
# Handle both URN formats
|
| 110 |
+
if "share" in post_id_urn:
|
| 111 |
+
post_id = post_id_urn.split(":")[-1]
|
| 112 |
+
post_url = f"https://www.linkedin.com/feed/update/urn:li:share:{post_id}/"
|
| 113 |
+
elif "activity" in post_id_urn:
|
| 114 |
+
post_id = post_id_urn.split(":")[-1]
|
| 115 |
+
post_url = f"https://www.linkedin.com/feed/update/urn:li:activity:{post_id}/"
|
| 116 |
+
else:
|
| 117 |
+
raise Exception("Unknown post URN format")
|
| 118 |
+
|
| 119 |
+
print(f"Post URL: {post_url}")
|
| 120 |
+
yield "✅ Post published successfully!", gr.update(value=f"### 🎉 [Click here to view your post on LinkedIn →]({post_url})", visible=True)
|
| 121 |
+
return
|
| 122 |
+
|
| 123 |
+
# If fetch failed or no posts found
|
| 124 |
+
yield "✅ Post published! (Could not auto-retrieve post link. Please check the LinkedIn page.)", gr.update(visible=False)
|
| 125 |
+
|
| 126 |
+
except Exception as fetch_error:
|
| 127 |
+
print(f"Error fetching post: {fetch_error}")
|
| 128 |
+
yield f"✅ Post published! (Error retrieving link: {fetch_error})", gr.update(visible=False)
|
| 129 |
+
|
| 130 |
else:
|
| 131 |
+
# 8. Handle POST errors gracefully
|
| 132 |
+
try:
|
| 133 |
+
error_data = response.json()
|
| 134 |
+
error_message = error_data.get('message', response.text)
|
| 135 |
+
except requests.exceptions.JSONDecodeError:
|
| 136 |
+
error_message = response.text
|
| 137 |
+
|
| 138 |
+
error_msg = f"❌ Error {response.status_code}: {error_message}"
|
| 139 |
+
print(error_msg)
|
| 140 |
+
yield error_msg, gr.update(visible=False)
|
| 141 |
|
| 142 |
except requests.exceptions.Timeout:
|
| 143 |
+
yield "❌ Error: Request timed out. Please try again.", gr.update(visible=False)
|
| 144 |
except requests.exceptions.RequestException as e:
|
| 145 |
+
yield f"❌ Network Error: {str(e)}", gr.update(visible=False)
|
| 146 |
except Exception as e:
|
| 147 |
+
yield f"❌ Error: {str(e)}", gr.update(visible=False)
|
| 148 |
+
|
| 149 |
|
| 150 |
+
# --- Polished Gradio Interface ---
|
| 151 |
+
|
| 152 |
+
custom_css = """
|
| 153 |
+
#title-link a { text-decoration: none; color: #0A66C2; }
|
| 154 |
+
#title-link a:hover { text-decoration: underline; }
|
| 155 |
+
#success-link-box {
|
| 156 |
+
text-align: center;
|
| 157 |
+
padding: 1rem;
|
| 158 |
+
background-color: #E7F3FF;
|
| 159 |
+
border-radius: 8px;
|
| 160 |
+
}
|
| 161 |
+
#success-link-box a { font-size: 1.1rem; font-weight: bold; }
|
| 162 |
+
#benefits-box {
|
| 163 |
+
background-color: #f7f9fb; /* Light background for the right panel */
|
| 164 |
+
padding: 15px;
|
| 165 |
+
border-radius: 8px;
|
| 166 |
+
height: 100%;
|
| 167 |
+
display: flex;
|
| 168 |
+
flex-direction: column;
|
| 169 |
+
align-items: center;
|
| 170 |
+
text-align: center;
|
| 171 |
+
}
|
| 172 |
+
"""
|
| 173 |
+
|
| 174 |
+
with gr.Blocks(theme=gr.themes.Soft(), css=custom_css) as demo:
|
| 175 |
+
|
| 176 |
+
# Global Header Section
|
| 177 |
+
with gr.Row():
|
| 178 |
+
gr.Image(
|
| 179 |
+
value="feedhire_logo.png", # NOTE: Ensure this image file is accessible or use a public URL
|
| 180 |
+
show_label=False,
|
| 181 |
+
show_download_button=False,
|
| 182 |
+
container=False,
|
| 183 |
+
height=80,
|
| 184 |
+
width=80,
|
| 185 |
+
)
|
| 186 |
|
| 187 |
+
gr.Markdown(
|
| 188 |
+
f"<h1 style='text-align: left;' id='title-link'>LinkedIn Job Poster for <a href='https://www.linkedin.com/company/{organisationNumber}/' target='_blank'>FEEDHIREJOBS PAGE</a></h1>"
|
| 189 |
+
)
|
| 190 |
+
|
| 191 |
+
gr.Markdown(
|
| 192 |
+
"<p style='text-align: left;'>Post job listings directly to the FeedHireJobs LinkedIn page</p>"
|
| 193 |
+
)
|
| 194 |
+
|
| 195 |
+
gr.Markdown("""
|
| 196 |
+
<p style="text-align: center;">
|
| 197 |
+
( Once your job is posted on our LinkedIn page, it will soon be visible at
|
| 198 |
+
<a href="https://feedhire.me/" target="_blank" style="color:#0073b1; text-decoration:none; font-weight:bold;">
|
| 199 |
+
FEEDHIRE.ME
|
| 200 |
+
</a> )
|
| 201 |
+
</p>
|
| 202 |
+
""")
|
| 203 |
+
|
| 204 |
+
|
| 205 |
# Show configuration status
|
| 206 |
if missing_vars:
|
| 207 |
+
gr.Warning(f"Configuration Error: Missing Secrets: {', '.join(missing_vars)}. Please check your Hugging Face Spaces configuration.")
|
| 208 |
+
|
| 209 |
+
|
| 210 |
|
| 211 |
+
## 🎯 Job Posting Panel
|
| 212 |
+
|
| 213 |
+
# Two-Column Layout (The core change)
|
| 214 |
+
with gr.Row(equal_height=True):
|
| 215 |
+
|
| 216 |
+
# LEFT COLUMN (FORM INPUTS)
|
| 217 |
+
with gr.Column(scale=1):
|
| 218 |
+
|
| 219 |
name_input = gr.Textbox(
|
| 220 |
+
label="Your Name",
|
| 221 |
+
placeholder="e.g., Aandu Pandu",
|
| 222 |
lines=1
|
| 223 |
)
|
| 224 |
job_post_input = gr.Textbox(
|
| 225 |
+
label="Job Post Content",
|
| 226 |
+
placeholder="Enter the full job post details here...\n\n- Role: ...\n- Responsibilities: ...\n- Description: ...\n- Location: ...\n- How to Apply: ...\n",
|
| 227 |
lines=10
|
| 228 |
)
|
| 229 |
+
|
| 230 |
+
submit_btn = gr.Button("🚀 Post to LinkedIn", variant="primary", size="lg")
|
| 231 |
+
|
| 232 |
+
output = gr.Textbox(label="Status", lines=2, interactive=False)
|
| 233 |
+
|
| 234 |
+
success_link = gr.Markdown(
|
| 235 |
+
"", # Will be dynamically set
|
| 236 |
+
visible=False,
|
| 237 |
+
elem_id="success-link-box" # For CSS styling
|
| 238 |
+
)
|
| 239 |
+
|
| 240 |
+
# RIGHT COLUMN (BENEFITS/MARKETING)
|
| 241 |
+
with gr.Column(scale=1, elem_id="benefits-box"):
|
| 242 |
+
|
| 243 |
+
gr.Markdown("### <span style='color: black;'>🌟 Why Post Jobs on FeedHire Page?</span>")
|
| 244 |
+
|
| 245 |
+
# Note: Replace this with a suitable marketing image if you have one.
|
| 246 |
+
# Using a public domain image placeholder for now.
|
| 247 |
+
gr.Image(
|
| 248 |
+
value="analytics_map.png", # Placeholder for a marketing graphic/image
|
| 249 |
+
show_label=False,
|
| 250 |
+
show_download_button=False,
|
| 251 |
+
container=False,
|
| 252 |
+
height=400,
|
| 253 |
+
width=600,
|
| 254 |
+
)
|
| 255 |
+
|
| 256 |
+
# Benefit 1
|
| 257 |
+
gr.Markdown("""
|
| 258 |
+
<span style="color:black;">
|
| 259 |
+
<b style="color:black;">🌍 Reach a Larger Audience:</b> Tap into FeedHire’s network and LinkedIn’s organic reach.
|
| 260 |
+
</span>
|
| 261 |
+
""")
|
| 262 |
+
|
| 263 |
+
gr.Markdown("""
|
| 264 |
+
<span style="color:black;">
|
| 265 |
+
<b style="color:black;">💬 Get More Responses:</b> Attract higher engagement and better applicants.
|
| 266 |
+
</span>
|
| 267 |
+
""")
|
| 268 |
+
|
| 269 |
+
# Benefit 2
|
| 270 |
+
gr.Markdown("""
|
| 271 |
+
<span style="color:black;">
|
| 272 |
+
<b style="color:black;"> 💯 Free to Use:</b> no hidden costs, no subscriptions, just genuine visibility.
|
| 273 |
+
</span>
|
| 274 |
+
""")
|
| 275 |
+
|
| 276 |
+
# Benefit 3
|
| 277 |
+
gr.Markdown("""
|
| 278 |
+
<span style="color:black;">
|
| 279 |
+
<b style="color:black;">🔗 Seamless Integration:</b> Post once — visible on <a href="https://www.linkedin.com/company/109539782/" target="_blank" style="color:blue; text-decoration:none; font-weight:bold;">LINKEDIN</a> and
|
| 280 |
+
<a href="https://feedhire.me/" target="_blank" style="color:blue; text-decoration:none; font-weight:bold;">FEEDHIRE.ME</a>.
|
| 281 |
+
</span>
|
| 282 |
+
""")
|
| 283 |
+
|
| 284 |
+
|
| 285 |
+
|
| 286 |
+
|
| 287 |
+
# --- Benefits Section ---
|
| 288 |
+
|
| 289 |
+
gr.Markdown("""
|
| 290 |
+
### 🌎 Why Choose FeedHire?
|
| 291 |
+
|
| 292 |
+
---
|
| 293 |
+
|
| 294 |
+
**1️⃣ Global Reach**
|
| 295 |
+
<span style="color: white;">
|
| 296 |
+
Connect with nearly <b style="color:white;">1,000+ active users</b> from around the world — including professionals from the
|
| 297 |
+
<b style="color:white;">US, UK, India, Nigeria, Germany</b>, and more.
|
| 298 |
+
</span>
|
| 299 |
+
|
| 300 |
+
---
|
| 301 |
+
|
| 302 |
+
**2️⃣ 100% Free to Use**
|
| 303 |
+
<span style="color: white;">
|
| 304 |
+
Post jobs without spending a penny — no hidden costs, no subscriptions, just genuine visibility.
|
| 305 |
+
</span>
|
| 306 |
+
|
| 307 |
+
---
|
| 308 |
+
|
| 309 |
+
**3️⃣ Safe & Legal**
|
| 310 |
+
<span style="color: white;">
|
| 311 |
+
All job posts are verified and comply with platform and regional policies, ensuring a transparent and trustworthy experience.
|
| 312 |
+
</span>
|
| 313 |
+
|
| 314 |
+
---
|
| 315 |
+
""")
|
| 316 |
+
|
| 317 |
+
|
| 318 |
+
# Click Action
|
| 319 |
submit_btn.click(
|
| 320 |
fn=post_to_linkedin,
|
| 321 |
inputs=[name_input, job_post_input],
|
| 322 |
+
outputs=[output, success_link],
|
| 323 |
+
show_progress="full"
|
| 324 |
)
|
| 325 |
|
| 326 |
if __name__ == "__main__":
|
| 327 |
+
demo.launch(allowed_paths=["."])
|