Seth commited on
Commit ·
187f183
1
Parent(s): c828443
update
Browse files- backend/app/main.py +28 -6
- backend/app/outreach_routes.py +2 -2
backend/app/main.py
CHANGED
|
@@ -114,6 +114,8 @@ app.include_router(outreach_router)
|
|
| 114 |
# Create uploads directory
|
| 115 |
UPLOAD_DIR = Path("/data/uploads")
|
| 116 |
UPLOAD_DIR.mkdir(parents=True, exist_ok=True)
|
|
|
|
|
|
|
| 117 |
UNIPILE_API_BASE = os.getenv("UNIPILE_API_BASE", "").rstrip("/")
|
| 118 |
UNIPILE_API_KEY = os.getenv("UNIPILE_API_KEY", "")
|
| 119 |
FRONTEND_ORIGIN = os.getenv("FRONTEND_ORIGIN", "").rstrip("/")
|
|
@@ -224,17 +226,26 @@ def _linkedin_public_identifier(raw: str) -> str:
|
|
| 224 |
return s.split("?")[0].strip("/").strip()
|
| 225 |
|
| 226 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 227 |
def _require_unipile_config():
|
| 228 |
-
|
|
|
|
| 229 |
raise HTTPException(
|
| 230 |
status_code=400,
|
| 231 |
-
detail="UniPile is not configured. Set UNIPILE_API_BASE and UNIPILE_API_KEY in backend env.",
|
| 232 |
)
|
| 233 |
|
| 234 |
|
| 235 |
def _unipile_headers():
|
|
|
|
| 236 |
return {
|
| 237 |
-
"X-API-KEY":
|
| 238 |
"accept": "application/json",
|
| 239 |
"content-type": "application/json",
|
| 240 |
}
|
|
@@ -246,7 +257,8 @@ def _unipile_call(method: str, path: str, payload: Optional[dict] = None):
|
|
| 246 |
Raises HTTPException only on transport failure (not on 4xx/5xx responses).
|
| 247 |
"""
|
| 248 |
_require_unipile_config()
|
| 249 |
-
|
|
|
|
| 250 |
try:
|
| 251 |
resp = requests.request(
|
| 252 |
method=method.upper(),
|
|
@@ -278,6 +290,14 @@ def _unipile_request(method: str, path: str, payload: Optional[dict] = None):
|
|
| 278 |
)
|
| 279 |
if isinstance(msg, (dict, list)):
|
| 280 |
msg = json.dumps(msg)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 281 |
raise HTTPException(
|
| 282 |
status_code=status,
|
| 283 |
detail=msg or f"UniPile error ({status}): {data if not isinstance(data, dict) else json.dumps(data)}",
|
|
@@ -1400,10 +1420,11 @@ async def create_unipile_mailbox_hosted_link(
|
|
| 1400 |
t.db.add(state)
|
| 1401 |
t.db.commit()
|
| 1402 |
expires_iso = expires_at.strftime("%Y-%m-%dT%H:%M:%S.%f")[:-3] + "Z"
|
|
|
|
| 1403 |
payload = {
|
| 1404 |
"type": "create",
|
| 1405 |
"providers": ["GOOGLE"],
|
| 1406 |
-
"api_url":
|
| 1407 |
"expiresOn": expires_iso,
|
| 1408 |
"notify_url": f"{origin}/api/unipile/mailbox/hosted-callback",
|
| 1409 |
"success_redirect_url": f"{origin}/settings?mailbox=connected",
|
|
@@ -1587,10 +1608,11 @@ async def create_unipile_linkedin_hosted_link(
|
|
| 1587 |
t.db.commit()
|
| 1588 |
|
| 1589 |
expires_iso = expires_at.strftime("%Y-%m-%dT%H:%M:%S.%f")[:-3] + "Z"
|
|
|
|
| 1590 |
payload = {
|
| 1591 |
"type": "create",
|
| 1592 |
"providers": ["LINKEDIN"],
|
| 1593 |
-
"api_url":
|
| 1594 |
"expiresOn": expires_iso,
|
| 1595 |
"notify_url": f"{origin}/api/unipile/linkedin/hosted-callback",
|
| 1596 |
"success_redirect_url": f"{origin}/settings?linkedin=connected",
|
|
|
|
| 114 |
# Create uploads directory
|
| 115 |
UPLOAD_DIR = Path("/data/uploads")
|
| 116 |
UPLOAD_DIR.mkdir(parents=True, exist_ok=True)
|
| 117 |
+
# UniPile credentials are read fresh on each request (see _load_unipile_env) so Spaces secret
|
| 118 |
+
# updates and stripping whitespace behave reliably. Module-level kept for backwards compat.
|
| 119 |
UNIPILE_API_BASE = os.getenv("UNIPILE_API_BASE", "").rstrip("/")
|
| 120 |
UNIPILE_API_KEY = os.getenv("UNIPILE_API_KEY", "")
|
| 121 |
FRONTEND_ORIGIN = os.getenv("FRONTEND_ORIGIN", "").rstrip("/")
|
|
|
|
| 226 |
return s.split("?")[0].strip("/").strip()
|
| 227 |
|
| 228 |
|
| 229 |
+
def _load_unipile_env() -> tuple[str, str]:
|
| 230 |
+
"""Read UniPile DSN and API key from the environment (strip whitespace / newlines from secrets)."""
|
| 231 |
+
base = (os.getenv("UNIPILE_API_BASE") or "").strip().rstrip("/")
|
| 232 |
+
key = (os.getenv("UNIPILE_API_KEY") or "").strip()
|
| 233 |
+
return base, key
|
| 234 |
+
|
| 235 |
+
|
| 236 |
def _require_unipile_config():
|
| 237 |
+
base, key = _load_unipile_env()
|
| 238 |
+
if not base or not key:
|
| 239 |
raise HTTPException(
|
| 240 |
status_code=400,
|
| 241 |
+
detail="UniPile is not configured. Set UNIPILE_API_BASE and UNIPILE_API_KEY in backend env (Hugging Face: Space → Settings → Secrets).",
|
| 242 |
)
|
| 243 |
|
| 244 |
|
| 245 |
def _unipile_headers():
|
| 246 |
+
_, key = _load_unipile_env()
|
| 247 |
return {
|
| 248 |
+
"X-API-KEY": key,
|
| 249 |
"accept": "application/json",
|
| 250 |
"content-type": "application/json",
|
| 251 |
}
|
|
|
|
| 257 |
Raises HTTPException only on transport failure (not on 4xx/5xx responses).
|
| 258 |
"""
|
| 259 |
_require_unipile_config()
|
| 260 |
+
base, _ = _load_unipile_env()
|
| 261 |
+
url = f"{base}{path}"
|
| 262 |
try:
|
| 263 |
resp = requests.request(
|
| 264 |
method=method.upper(),
|
|
|
|
| 290 |
)
|
| 291 |
if isinstance(msg, (dict, list)):
|
| 292 |
msg = json.dumps(msg)
|
| 293 |
+
err_type = _safe_str(data.get("type"))
|
| 294 |
+
if status == 401 and err_type == "errors/missing_credentials":
|
| 295 |
+
msg = (
|
| 296 |
+
"UniPile rejected the request (missing or invalid API credentials). "
|
| 297 |
+
"In Hugging Face Space → Settings → Secrets, set UNIPILE_API_KEY to your UniPile API access token "
|
| 298 |
+
"(Dashboard → API keys) and UNIPILE_API_BASE to your UniPile API URL (no trailing slash). "
|
| 299 |
+
"Remove accidental quotes or spaces when pasting; redeploy/restart the Space after saving."
|
| 300 |
+
)
|
| 301 |
raise HTTPException(
|
| 302 |
status_code=status,
|
| 303 |
detail=msg or f"UniPile error ({status}): {data if not isinstance(data, dict) else json.dumps(data)}",
|
|
|
|
| 1420 |
t.db.add(state)
|
| 1421 |
t.db.commit()
|
| 1422 |
expires_iso = expires_at.strftime("%Y-%m-%dT%H:%M:%S.%f")[:-3] + "Z"
|
| 1423 |
+
api_base, _ = _load_unipile_env()
|
| 1424 |
payload = {
|
| 1425 |
"type": "create",
|
| 1426 |
"providers": ["GOOGLE"],
|
| 1427 |
+
"api_url": api_base,
|
| 1428 |
"expiresOn": expires_iso,
|
| 1429 |
"notify_url": f"{origin}/api/unipile/mailbox/hosted-callback",
|
| 1430 |
"success_redirect_url": f"{origin}/settings?mailbox=connected",
|
|
|
|
| 1608 |
t.db.commit()
|
| 1609 |
|
| 1610 |
expires_iso = expires_at.strftime("%Y-%m-%dT%H:%M:%S.%f")[:-3] + "Z"
|
| 1611 |
+
api_base, _ = _load_unipile_env()
|
| 1612 |
payload = {
|
| 1613 |
"type": "create",
|
| 1614 |
"providers": ["LINKEDIN"],
|
| 1615 |
+
"api_url": api_base,
|
| 1616 |
"expiresOn": expires_iso,
|
| 1617 |
"notify_url": f"{origin}/api/unipile/linkedin/hosted-callback",
|
| 1618 |
"success_redirect_url": f"{origin}/settings?linkedin=connected",
|
backend/app/outreach_routes.py
CHANGED
|
@@ -95,8 +95,8 @@ def _linkedin_public_identifier(raw: str) -> str:
|
|
| 95 |
|
| 96 |
|
| 97 |
def _unipile_env() -> Tuple[str, str]:
|
| 98 |
-
base = os.getenv("UNIPILE_API_BASE"
|
| 99 |
-
key = os.getenv("UNIPILE_API_KEY"
|
| 100 |
return base, key
|
| 101 |
|
| 102 |
|
|
|
|
| 95 |
|
| 96 |
|
| 97 |
def _unipile_env() -> Tuple[str, str]:
|
| 98 |
+
base = (os.getenv("UNIPILE_API_BASE") or "").strip().rstrip("/")
|
| 99 |
+
key = (os.getenv("UNIPILE_API_KEY") or "").strip()
|
| 100 |
return base, key
|
| 101 |
|
| 102 |
|