Spaces:
Runtime error
Runtime error
| import os | |
| import io | |
| import time | |
| import requests | |
| import csv | |
| import streamlit as st | |
| # ----------------------------------------------------------------------------- | |
| # LinkedIn Outreach Intake App | |
| # | |
| # This Streamlit application provides a simple interface for uploading individual | |
| # LinkedIn contacts or an entire Connections CSV. It forwards the data to two | |
| # n8n webhooks: one for appending contact data to a Google Sheet (intake) and | |
| # another to trigger the outreach workflow that sends email via your Gmail | |
| # account. All configurable values—such as webhook URLs and diagram image | |
| # location—are provided through environment variables so that no secrets are | |
| # hard‑coded in the repository. The outreach itself is handled by n8n and does | |
| # not rely on any paid AI services. | |
| # | |
| # Required environment variables: | |
| # N8N_INTAKE_WEBHOOK – URL of the webhook that writes to Sheet‑1. | |
| # N8N_OUTREACH_WEBHOOK – URL of the webhook that triggers the outreach run. | |
| # Optional environment variables: | |
| # DIAGRAM_URL – Public URL of the flow diagram for display. | |
| # | |
| # To deploy this app in a Hugging Face Space, provide these variables in the | |
| # repository settings (Settings → Variables and secrets). | |
| # ----------------------------------------------------------------------------- | |
| INTAKE_URL = os.environ.get("N8N_INTAKE_WEBHOOK") | |
| OUTREACH_URL = os.environ.get("N8N_OUTREACH_WEBHOOK") | |
| DIAGRAM_URL = os.environ.get("DIAGRAM_URL", "") | |
| st.set_page_config( | |
| page_title="LinkedIn Outreach Intake", | |
| page_icon="📇", | |
| layout="centered", | |
| ) | |
| st.title("LinkedIn → n8n Intake → Outreach") | |
| if DIAGRAM_URL: | |
| st.image(DIAGRAM_URL, caption="Flow overview", use_column_width=True) | |
| st.write( | |
| "This app lets you add LinkedIn contacts (either individually or via CSV) " | |
| "and trigger the outreach workflow that sends tailored emails." | |
| ) | |
| def post_to_intake(data: dict) -> int: | |
| """Send JSON payload to the intake webhook. Returns HTTP status.""" | |
| if not INTAKE_URL: | |
| return 0 | |
| try: | |
| resp = requests.post(INTAKE_URL, json=data, timeout=30) | |
| return resp.status_code | |
| except Exception: | |
| return 0 | |
| def run_outreach() -> int: | |
| """Trigger the outreach workflow via its webhook. Returns HTTP status.""" | |
| if not OUTREACH_URL: | |
| return 0 | |
| try: | |
| resp = requests.post(OUTREACH_URL, json={}, timeout=30) | |
| return resp.status_code | |
| except Exception: | |
| return 0 | |
| with st.expander("Add a single contact"): | |
| c1, c2 = st.columns(2) | |
| name = c1.text_input("Name *") | |
| title = c2.text_input("Title *") | |
| company = c1.text_input("Company *") | |
| email = c2.text_input("Email (optional)") | |
| linkedin = st.text_input("LinkedIn URL *") | |
| cbt = st.selectbox("Careers Board Type", ["", "lever", "greenhouse", "url"]) | |
| cbk = st.text_input("Careers Board Key (subdomain/slug)") | |
| cbu = st.text_input("Careers URL (for custom boards)") | |
| if st.button("Submit contact"): | |
| # Validate required fields | |
| if not (name and title and company and linkedin): | |
| st.error("Please fill in all required fields marked with *.") | |
| elif not INTAKE_URL: | |
| st.error("N8N_INTAKE_WEBHOOK environment variable not set.") | |
| else: | |
| payload = { | |
| "name": name, | |
| "title": title, | |
| "company": company, | |
| "email": email, | |
| "linkedin_url": linkedin, | |
| "careers_board_type": cbt, | |
| "careers_board_key": cbk, | |
| "careers_url": cbu, | |
| } | |
| status = post_to_intake(payload) | |
| if status == 200: | |
| st.success("Contact saved successfully.") | |
| else: | |
| st.warning(f"Failed to submit: status {status}.") | |
| st.divider() | |
| st.subheader("Bulk import LinkedIn Connections.csv") | |
| csv_file = st.file_uploader( | |
| "Upload your Connections.csv (exported from LinkedIn)", | |
| type=["csv"], | |
| ) | |
| st.caption( | |
| "Your CSV should contain at least columns for name, title, company, " | |
| "email, and LinkedIn profile URL. Missing columns will be filled with blanks." | |
| ) | |
| if csv_file and st.button("Send CSV to n8n"): | |
| if not INTAKE_URL: | |
| st.error("N8N_INTAKE_WEBHOOK environment variable not set.") | |
| else: | |
| try: | |
| # Reset file pointer and decode the uploaded bytes into a string | |
| csv_bytes = csv_file.getvalue() | |
| csv_text = csv_bytes.decode("utf-8", errors="ignore") | |
| reader = csv.DictReader(io.StringIO(csv_text)) | |
| imported = 0 | |
| failed = 0 | |
| for row in reader: | |
| # Normalize keys: strip whitespace and convert spaces to underscores | |
| normalized = {k.strip().lower().replace(" ", "_"): v for k, v in row.items() if k} | |
| payload = { | |
| "name": normalized.get("name", ""), | |
| "title": normalized.get("title", ""), | |
| "company": normalized.get("company", ""), | |
| "email": normalized.get("email", ""), | |
| # LinkedIn profile URL column may be labelled 'linkedin_url' or 'linkedin' | |
| "linkedin_url": normalized.get("linkedin_url", normalized.get("linkedin", "")), | |
| "careers_board_type": "", | |
| "careers_board_key": "", | |
| "careers_url": "", | |
| } | |
| status = post_to_intake(payload) | |
| if status == 200: | |
| imported += 1 | |
| else: | |
| failed += 1 | |
| # Sleep briefly to avoid overwhelming n8n | |
| time.sleep(0.25) | |
| st.success(f"Imported {imported} contacts; {failed} failed.") | |
| except Exception as exc: | |
| st.error(f"Error processing CSV: {exc}") | |
| st.divider() | |
| st.subheader("Trigger Outreach Workflow") | |
| st.write( | |
| "When you trigger outreach, the app calls your n8n webhook to read all " | |
| "contacts from Sheet‑1 and send emails. Ensure your sheet is populated " | |
| "and your Gmail credential is configured correctly in n8n before running." | |
| ) | |
| if st.button("Run Outreach now"): | |
| if not OUTREACH_URL: | |
| st.error("N8N_OUTREACH_WEBHOOK environment variable not set.") | |
| else: | |
| status = run_outreach() | |
| if status == 200: | |
| st.success("Outreach workflow triggered. Check your n8n logs.") | |
| else: | |
| st.warning(f"Failed to trigger outreach: status {status}.") |