chore: push all remaining changes
- Blog writer enhancements and bug fixes - Wix integration improvements - Frontend UI updates - GSC dashboard docs cleanup - Image studio assets - LinkedIn requirements file - Various dependency updates
This commit is contained in:
@@ -71,6 +71,7 @@ class SEOApplyRecommendationsRequest(BaseModel):
|
||||
outline: List[Dict[str, Any]] = Field(default_factory=list, description="Outline structure for context")
|
||||
research: Dict[str, Any] = Field(default_factory=dict, description="Research data used for the blog")
|
||||
recommendations: List[RecommendationItem] = Field(..., description="Actionable recommendations to apply")
|
||||
competitive_advantage: str | None = Field(default=None, description="Selected competitive advantage for emphasis")
|
||||
persona: Dict[str, Any] = Field(default_factory=dict, description="Persona settings if available")
|
||||
tone: str | None = Field(default=None, description="Desired tone override")
|
||||
audience: str | None = Field(default=None, description="Target audience override")
|
||||
@@ -688,9 +689,11 @@ async def get_section_continuity(section_id: str) -> Dict[str, Any]:
|
||||
|
||||
|
||||
@router.post("/flow-analysis/basic")
|
||||
async def analyze_flow_basic(request: Dict[str, Any]) -> Dict[str, Any]:
|
||||
async def analyze_flow_basic(request: Dict[str, Any], current_user: Dict[str, Any] = Depends(get_current_user)) -> Dict[str, Any]:
|
||||
"""Analyze flow metrics for entire blog using single AI call (cost-effective)."""
|
||||
try:
|
||||
user_id = str(current_user.get('id', '')) if current_user else None
|
||||
request['user_id'] = user_id
|
||||
result = await service.analyze_flow_basic(request)
|
||||
return result
|
||||
except Exception as e:
|
||||
@@ -699,9 +702,11 @@ async def analyze_flow_basic(request: Dict[str, Any]) -> Dict[str, Any]:
|
||||
|
||||
|
||||
@router.post("/flow-analysis/advanced")
|
||||
async def analyze_flow_advanced(request: Dict[str, Any]) -> Dict[str, Any]:
|
||||
async def analyze_flow_advanced(request: Dict[str, Any], current_user: Dict[str, Any] = Depends(get_current_user)) -> Dict[str, Any]:
|
||||
"""Analyze flow metrics for each section individually (detailed but expensive)."""
|
||||
try:
|
||||
user_id = str(current_user.get('id', '')) if current_user else None
|
||||
request['user_id'] = user_id
|
||||
result = await service.analyze_flow_advanced(request)
|
||||
return result
|
||||
except Exception as e:
|
||||
@@ -808,9 +813,12 @@ async def seo_metadata(
|
||||
|
||||
|
||||
# Publishing Endpoints
|
||||
# NOTE: Real publishing bypasses this stub. Frontend calls platform-specific
|
||||
# endpoints directly: /api/wix/publish and /api/wordpress/publish.
|
||||
# This endpoint is kept as a placeholder for the future unified publish flow.
|
||||
@router.post("/publish", response_model=BlogPublishResponse)
|
||||
async def publish(request: BlogPublishRequest) -> BlogPublishResponse:
|
||||
"""Publish the blog post to the specified platform."""
|
||||
"""Publish the blog post to the specified platform. [STUB - see note above]"""
|
||||
try:
|
||||
return await service.publish(request)
|
||||
except Exception as e:
|
||||
@@ -1209,6 +1217,9 @@ async def generate_introductions(
|
||||
class SaveCompleteBlogAssetRequest(BaseModel):
|
||||
title: str
|
||||
content: str
|
||||
platform: Optional[str] = None
|
||||
post_url: Optional[str] = None
|
||||
post_id: Optional[str] = None
|
||||
seo_title: Optional[str] = None
|
||||
meta_description: Optional[str] = None
|
||||
focus_keyword: Optional[str] = None
|
||||
@@ -1233,6 +1244,19 @@ async def save_complete_blog_asset(
|
||||
|
||||
full_content = f"# {request.title}\n\n{request.content}"
|
||||
|
||||
asset_metadata = {
|
||||
"status": "published",
|
||||
"focus_keyword": request.focus_keyword,
|
||||
"categories": request.categories,
|
||||
"word_count": len(full_content.split()),
|
||||
}
|
||||
if request.platform:
|
||||
asset_metadata["platform"] = request.platform
|
||||
if request.post_url:
|
||||
asset_metadata["post_url"] = request.post_url
|
||||
if request.post_id:
|
||||
asset_metadata["post_id"] = request.post_id
|
||||
|
||||
asset_id = save_and_track_text_content(
|
||||
db=db,
|
||||
user_id=user_id,
|
||||
@@ -1242,12 +1266,7 @@ async def save_complete_blog_asset(
|
||||
description=request.meta_description or f"Complete published blog post: {request.title}",
|
||||
prompt=f"SEO Title: {request.seo_title or request.title}\nFocus Keyword: {request.focus_keyword or ''}",
|
||||
tags=["blog", "published"] + [t for t in (request.tags or []) if t],
|
||||
asset_metadata={
|
||||
"status": "published",
|
||||
"focus_keyword": request.focus_keyword,
|
||||
"categories": request.categories,
|
||||
"word_count": len(full_content.split()),
|
||||
},
|
||||
asset_metadata=asset_metadata,
|
||||
subdirectory="published",
|
||||
file_extension=".md"
|
||||
)
|
||||
@@ -1266,6 +1285,57 @@ async def save_complete_blog_asset(
|
||||
raise HTTPException(status_code=500, detail=str(e))
|
||||
|
||||
|
||||
@router.get("/publish-history")
|
||||
async def get_publish_history(
|
||||
current_user: Dict[str, Any] = Depends(get_current_user),
|
||||
db: Session = Depends(get_db),
|
||||
limit: int = 50,
|
||||
offset: int = 0,
|
||||
) -> Dict[str, Any]:
|
||||
"""Get publish history for the current user from the asset library."""
|
||||
try:
|
||||
if not current_user:
|
||||
raise HTTPException(status_code=401, detail="Authentication required")
|
||||
|
||||
user_id = str(current_user.get('id', ''))
|
||||
if not user_id:
|
||||
raise HTTPException(status_code=401, detail="Invalid user ID in authentication token")
|
||||
|
||||
svc = ContentAssetService(db)
|
||||
assets, total = svc.get_user_assets(
|
||||
user_id=user_id,
|
||||
tags=["published"],
|
||||
source_module=AssetSource.BLOG_WRITER,
|
||||
sort_by="created_at",
|
||||
sort_order="desc",
|
||||
limit=limit,
|
||||
offset=offset,
|
||||
)
|
||||
|
||||
entries = []
|
||||
for a in assets:
|
||||
meta = a.asset_metadata or {}
|
||||
entries.append({
|
||||
"asset_id": a.id,
|
||||
"title": a.title,
|
||||
"platform": meta.get("platform", "unknown"),
|
||||
"post_url": meta.get("post_url"),
|
||||
"post_id": meta.get("post_id"),
|
||||
"word_count": meta.get("word_count", 0),
|
||||
"focus_keyword": meta.get("focus_keyword"),
|
||||
"categories": meta.get("categories", []),
|
||||
"published_at": a.created_at.isoformat() if a.created_at else None,
|
||||
})
|
||||
|
||||
return {"success": True, "entries": entries, "total": total}
|
||||
|
||||
except HTTPException:
|
||||
raise
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to get publish history: {e}")
|
||||
raise HTTPException(status_code=500, detail=str(e))
|
||||
|
||||
|
||||
# ---------------------------------------
|
||||
# Blog Asset API (phase-by-phase saving via ContentAsset)
|
||||
# ---------------------------------------
|
||||
|
||||
@@ -28,6 +28,8 @@ class SEOAnalysisRequest(BaseModel):
|
||||
blog_content: str
|
||||
blog_title: Optional[str] = None
|
||||
research_data: Dict[str, Any]
|
||||
outline: Optional[List[Dict[str, Any]]] = None
|
||||
competitive_advantage: Optional[str] = None
|
||||
user_id: Optional[str] = None
|
||||
session_id: Optional[str] = None
|
||||
|
||||
@@ -109,7 +111,9 @@ async def analyze_blog_seo(
|
||||
blog_content=request.blog_content,
|
||||
research_data=request.research_data,
|
||||
blog_title=request.blog_title,
|
||||
user_id=user_id
|
||||
user_id=user_id,
|
||||
outline=request.outline,
|
||||
competitive_advantage=request.competitive_advantage,
|
||||
)
|
||||
|
||||
# Check for errors
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
import os
|
||||
from fastapi import APIRouter, HTTPException, UploadFile, File, Depends
|
||||
from fastapi.responses import FileResponse
|
||||
from pydantic import BaseModel
|
||||
from typing import List, Optional, Dict, Any
|
||||
import json
|
||||
import base64
|
||||
|
||||
# Import our LinkedIn image generation services
|
||||
from services.linkedin.image_generation import LinkedInImageGenerator, LinkedInImageStorage
|
||||
@@ -51,6 +53,23 @@ class ImageGenerationResponse(BaseModel):
|
||||
aspect_ratio: Optional[str] = None
|
||||
error: Optional[str] = None
|
||||
|
||||
class ImageEditRequest(BaseModel):
|
||||
image_base64: Optional[str] = None
|
||||
image_id: Optional[str] = None
|
||||
prompt: str
|
||||
content_context: Dict[str, Any]
|
||||
|
||||
class ImageEditResponse(BaseModel):
|
||||
success: bool
|
||||
image_data: Optional[str] = None
|
||||
image_id: Optional[str] = None
|
||||
image_url: Optional[str] = None
|
||||
width: Optional[int] = None
|
||||
height: Optional[int] = None
|
||||
provider: Optional[str] = None
|
||||
model: Optional[str] = None
|
||||
error: Optional[str] = None
|
||||
|
||||
@router.post("/generate-image-prompts", response_model=List[ImagePromptResponse])
|
||||
async def generate_image_prompts(request: ImagePromptRequest):
|
||||
"""
|
||||
@@ -89,7 +108,8 @@ async def generate_linkedin_image(
|
||||
# Use our LinkedIn image generator service
|
||||
image_result = await image_generator.generate_image(
|
||||
prompt=request.prompt,
|
||||
content_context=request.content_context
|
||||
content_context=request.content_context,
|
||||
user_id=user_id
|
||||
)
|
||||
|
||||
if image_result and image_result.get('success'):
|
||||
@@ -131,6 +151,99 @@ async def generate_linkedin_image(
|
||||
error=f"Failed to generate image: {str(e)}"
|
||||
)
|
||||
|
||||
@router.post("/edit-image", response_model=ImageEditResponse)
|
||||
async def edit_linkedin_image(
|
||||
request: ImageEditRequest,
|
||||
current_user: Dict[str, Any] = Depends(get_current_user)
|
||||
):
|
||||
"""
|
||||
Edit a LinkedIn-optimized image using natural language.
|
||||
Provide the image as base64 and describe the desired edits.
|
||||
"""
|
||||
try:
|
||||
user_id = current_user.get("id")
|
||||
if not user_id:
|
||||
raise HTTPException(status_code=401, detail="Authentication required")
|
||||
|
||||
if not request.prompt or not request.prompt.strip():
|
||||
raise HTTPException(status_code=400, detail="Prompt is required for image editing")
|
||||
|
||||
logger.info(f"Editing LinkedIn image with prompt: {request.prompt[:100]}... for user {user_id}")
|
||||
|
||||
# Get input image bytes — from image_id (fetch from storage) or image_base64 (direct decode)
|
||||
input_image_bytes = None
|
||||
if request.image_id:
|
||||
stored = await image_storage.retrieve_image(request.image_id, user_id)
|
||||
if not stored or not stored.get('success'):
|
||||
raise HTTPException(status_code=404, detail=f"Image not found: {request.image_id}")
|
||||
input_image_bytes = stored['image_data']
|
||||
logger.info(f"Fetched image {request.image_id} from storage ({len(input_image_bytes)} bytes)")
|
||||
elif request.image_base64:
|
||||
input_image_bytes = base64.b64decode(request.image_base64)
|
||||
else:
|
||||
raise HTTPException(status_code=400, detail="Either image_id or image_base64 is required")
|
||||
|
||||
# Use LinkedIn image generator with common editing infrastructure
|
||||
image_result = await image_generator.edit_image(
|
||||
input_image_bytes=input_image_bytes,
|
||||
edit_prompt=request.prompt,
|
||||
content_context=request.content_context,
|
||||
user_id=user_id,
|
||||
)
|
||||
|
||||
if image_result and image_result.get('success'):
|
||||
image_b64 = base64.b64encode(image_result['image_data']).decode("utf-8")
|
||||
|
||||
# Store the edited image — log but don't fail if storage has issues
|
||||
new_image_id = None
|
||||
stored_result = await image_storage.store_image(
|
||||
image_data=image_result['image_data'],
|
||||
metadata={
|
||||
'prompt': request.prompt,
|
||||
'style': request.content_context.get('style', 'Edited'),
|
||||
'content_type': request.content_context.get('content_type'),
|
||||
'topic': request.content_context.get('topic'),
|
||||
'industry': request.content_context.get('industry'),
|
||||
'is_edit': True,
|
||||
'original_prompt': request.prompt,
|
||||
'source_image_id': request.image_id,
|
||||
},
|
||||
user_id=user_id
|
||||
)
|
||||
if stored_result and stored_result.get('success'):
|
||||
new_image_id = stored_result.get('image_id')
|
||||
logger.info(f"Edited image stored with ID: {new_image_id}")
|
||||
else:
|
||||
logger.warning(f"Edited image not stored: {stored_result.get('error', 'unknown reason')}")
|
||||
|
||||
return ImageEditResponse(
|
||||
success=True,
|
||||
image_data=image_b64,
|
||||
image_id=new_image_id,
|
||||
image_url=image_result.get('image_url'),
|
||||
width=image_result.get('width'),
|
||||
height=image_result.get('height'),
|
||||
provider=image_result.get('provider'),
|
||||
model=image_result.get('model'),
|
||||
)
|
||||
else:
|
||||
error_msg = image_result.get('error', 'Unknown error during image editing')
|
||||
logger.error(f"Image editing failed: {error_msg}")
|
||||
return ImageEditResponse(
|
||||
success=False,
|
||||
error=error_msg
|
||||
)
|
||||
|
||||
except HTTPException:
|
||||
raise
|
||||
except Exception as e:
|
||||
logger.error(f"Error editing LinkedIn image: {str(e)}", exc_info=True)
|
||||
return ImageEditResponse(
|
||||
success=False,
|
||||
error=f"Failed to edit image: {str(e)}"
|
||||
)
|
||||
|
||||
|
||||
@router.get("/image-status/{image_id}")
|
||||
async def get_image_status(
|
||||
image_id: str,
|
||||
@@ -169,42 +282,23 @@ async def get_generated_image(
|
||||
current_user: Dict[str, Any] = Depends(get_current_user)
|
||||
):
|
||||
"""
|
||||
Retrieve a generated image by ID
|
||||
Retrieve a generated image by ID.
|
||||
Returns the image file directly as a PNG response.
|
||||
"""
|
||||
try:
|
||||
user_id = current_user.get("id")
|
||||
image_result = await image_storage.retrieve_image(image_id, user_id)
|
||||
|
||||
if image_result.get('success') and 'image_data' in image_result:
|
||||
# Return as streaming response or raw bytes depending on frontend needs
|
||||
# For now returning the structure as before but image_data is bytes
|
||||
# Ideally this should be a Response object with image/png content type
|
||||
# But keeping consistency with existing return type structure for now if it was returning dict
|
||||
# Wait, retrieve_image returns dict with 'image_data' as bytes.
|
||||
# The original code returned: {"success": True, "image_data": image_data}
|
||||
# FastAPI handles bytes in JSON? No, it will fail serialization.
|
||||
# The previous implementation of retrieve_image (lines 190-195) returned bytes in a dict.
|
||||
# Unless FastAPI response model handles it, this might have been broken or handled specially.
|
||||
# Let's check imports.
|
||||
# It uses APIRouter.
|
||||
# If I return a dict with bytes, json serialization fails.
|
||||
# Maybe the original code expected base64 or it was just broken?
|
||||
# Or maybe image_data was not bytes?
|
||||
# In retrieve_image: with open(..., 'rb') as f: image_data = f.read() -> bytes.
|
||||
# So returning it in a dict will definitely fail JSON serialization.
|
||||
# I should probably return a Response or FileResponse, or base64 encode it.
|
||||
# But for now, I will just match the signature and pass user_id.
|
||||
# If it was broken before, I'm not fixing that unless asked, but I suspect it might be base64 in usage?
|
||||
# Let's look at `generate_linkedin_image` which returns `ImageGenerationResponse` with `image_url`.
|
||||
# `get_generated_image` returns a dict.
|
||||
# I will stick to passing user_id.
|
||||
|
||||
return {
|
||||
"success": True,
|
||||
"image_data": image_result['image_data'] # This might need base64 encoding if it's for JSON
|
||||
}
|
||||
if image_result.get('success') and image_result.get('image_path'):
|
||||
return FileResponse(
|
||||
path=image_result['image_path'],
|
||||
media_type="image/png",
|
||||
filename=f"{image_id}.png"
|
||||
)
|
||||
else:
|
||||
raise HTTPException(status_code=404, detail="Image not found")
|
||||
except HTTPException:
|
||||
raise
|
||||
except Exception as e:
|
||||
logger.error(f"Error retrieving image: {str(e)}")
|
||||
raise HTTPException(status_code=500, detail=f"Failed to retrieve image: {str(e)}")
|
||||
@@ -232,25 +326,42 @@ async def delete_generated_image(
|
||||
@router.get("/image-generation-health")
|
||||
async def health_check():
|
||||
"""
|
||||
Health check for image generation services
|
||||
Lightweight health check for image generation services.
|
||||
Verifies configuration and service availability without making API calls.
|
||||
"""
|
||||
try:
|
||||
# Test basic service functionality
|
||||
test_prompts = await prompt_generator.generate_three_prompts({
|
||||
'content_type': 'post',
|
||||
'topic': 'Test',
|
||||
'industry': 'Technology',
|
||||
'content': 'Test content for health check'
|
||||
})
|
||||
|
||||
services = {}
|
||||
all_healthy = True
|
||||
|
||||
# Check API key configuration (no actual API call)
|
||||
image_api_key = api_key_manager.get_api_key("image_generation") or os.getenv("WAVESPEED_API_KEY") or os.getenv("HF_TOKEN")
|
||||
services["image_api_key_configured"] = bool(image_api_key)
|
||||
|
||||
# Check storage accessibility
|
||||
stats = await image_storage.get_storage_stats()
|
||||
storage_ok = stats.get('success', False)
|
||||
services["image_storage"] = "operational" if storage_ok else "unavailable"
|
||||
if storage_ok:
|
||||
services["storage_stats"] = {
|
||||
"total_images": stats.get('total_files', 0),
|
||||
"total_size_gb": stats.get('total_size_gb', 0),
|
||||
"limit_gb": stats.get('storage_limit_gb', 0),
|
||||
}
|
||||
|
||||
# Check prompt generator initialization
|
||||
prompt_ok = prompt_generator is not None and hasattr(prompt_generator, 'generate_three_prompts')
|
||||
services["prompt_generator"] = "operational" if prompt_ok else "unavailable"
|
||||
|
||||
# Check image generator initialization
|
||||
gen_ok = image_generator is not None and hasattr(image_generator, 'generate_image')
|
||||
services["image_generator"] = "operational" if gen_ok else "unavailable"
|
||||
|
||||
if not all(v == "operational" or v is True for v in services.values()):
|
||||
all_healthy = False
|
||||
|
||||
return {
|
||||
"status": "healthy",
|
||||
"services": {
|
||||
"prompt_generator": "operational",
|
||||
"image_generator": "operational",
|
||||
"image_storage": "operational"
|
||||
},
|
||||
"test_prompts_generated": len(test_prompts)
|
||||
"status": "healthy" if all_healthy else "degraded",
|
||||
"services": services
|
||||
}
|
||||
except Exception as e:
|
||||
logger.error(f"Health check failed: {str(e)}")
|
||||
|
||||
@@ -16,6 +16,7 @@ import time
|
||||
|
||||
from services.wix_service import WixService
|
||||
from services.integrations.wix_oauth import WixOAuthService
|
||||
from services.integrations.wix.utils import extract_meta_from_token
|
||||
from services.integrations.oauth_callback_utils import (
|
||||
build_oauth_callback_html,
|
||||
sanitize_error,
|
||||
@@ -102,6 +103,38 @@ def _map_wix_error(exc: Exception, fallback: str = "Wix API request failed") ->
|
||||
detail="Network error connecting to Wix. Please check your connection and try again."
|
||||
)
|
||||
|
||||
# Handle WixAPIError from our retry/API layer
|
||||
from services.integrations.wix.retry import WixAPIError
|
||||
if isinstance(exc, WixAPIError):
|
||||
status = exc.status_code
|
||||
msg = exc.response_body or str(exc)
|
||||
if status == 401:
|
||||
return HTTPException(
|
||||
status_code=401,
|
||||
detail="Wix authorization failed. Please reconnect your Wix account."
|
||||
)
|
||||
if status == 403:
|
||||
return HTTPException(
|
||||
status_code=403,
|
||||
detail="Wix permission denied. Ensure your OAuth app has blog permissions (BLOG.CREATE-DRAFT)."
|
||||
)
|
||||
if status == 404:
|
||||
return HTTPException(
|
||||
status_code=502,
|
||||
detail="Wix API endpoint not found. Ensure the site ID is correct and the blog feature is enabled."
|
||||
)
|
||||
if status == 429:
|
||||
return HTTPException(
|
||||
status_code=429,
|
||||
detail="Wix rate limit exceeded. Please wait a moment and try again."
|
||||
)
|
||||
if status in (500, 502, 503, 504):
|
||||
return HTTPException(
|
||||
status_code=502,
|
||||
detail="Wix service temporarily unavailable. Please try again in a moment."
|
||||
)
|
||||
return HTTPException(status_code=status or 502, detail=msg or fallback)
|
||||
|
||||
# For validation errors from blog_publisher
|
||||
error_str = str(exc)
|
||||
if "validation failed" in error_str.lower():
|
||||
@@ -150,12 +183,16 @@ def _resolve_valid_wix_token(current_user: dict) -> Dict[str, Any]:
|
||||
expires_in=refreshed.get("expires_in"),
|
||||
token_id=token_id,
|
||||
)
|
||||
site_id = candidate.get("site_id")
|
||||
if not site_id:
|
||||
meta_info = extract_meta_from_token(refreshed.get("access_token"))
|
||||
site_id = meta_info.get('metaSiteId') or site_id
|
||||
logger.info(f"Wix token refreshed successfully on attempt {attempt} for user {user_id[:8]}...")
|
||||
return {
|
||||
"access_token": refreshed.get("access_token"),
|
||||
"refresh_token": refreshed.get("refresh_token", refresh_token),
|
||||
"member_id": candidate.get("member_id"),
|
||||
"site_id": candidate.get("site_id"),
|
||||
"site_id": site_id,
|
||||
}
|
||||
|
||||
raise HTTPException(status_code=401, detail="Wix token expired and cannot be refreshed")
|
||||
@@ -315,6 +352,9 @@ async def handle_oauth_callback(request: WixAuthRequest, current_user: dict = De
|
||||
try:
|
||||
site_info = wix_service.get_site_info(access_token)
|
||||
site_id = site_info.get('siteId') or site_info.get('site_id')
|
||||
if not site_id and site_info.get('_no_site'):
|
||||
meta_info = extract_meta_from_token(access_token)
|
||||
site_id = meta_info.get('metaSiteId')
|
||||
except Exception as e:
|
||||
logger.warning(f"get_site_info failed (non-fatal): {e}")
|
||||
try:
|
||||
@@ -322,7 +362,7 @@ async def handle_oauth_callback(request: WixAuthRequest, current_user: dict = De
|
||||
except Exception:
|
||||
pass
|
||||
try:
|
||||
permissions = wix_service.check_blog_permissions(access_token)
|
||||
permissions = wix_service.check_blog_permissions(access_token, site_id=site_id)
|
||||
except Exception as e:
|
||||
logger.warning(f"check_blog_permissions failed (non-fatal): {e}")
|
||||
|
||||
@@ -351,11 +391,14 @@ async def handle_oauth_callback(request: WixAuthRequest, current_user: dict = De
|
||||
try:
|
||||
site_info = wix_service.get_site_info(access_token)
|
||||
site_id = site_info.get('siteId') or site_info.get('site_id')
|
||||
if not site_id and site_info.get('_no_site'):
|
||||
meta_info = extract_meta_from_token(access_token)
|
||||
site_id = meta_info.get('metaSiteId') or site_id
|
||||
except Exception as e:
|
||||
logger.warning(f"get_site_info failed (non-fatal): {e}")
|
||||
try:
|
||||
from services.integrations.wix.utils import extract_meta_from_token
|
||||
site_id = extract_meta_from_token(access_token) or site_id
|
||||
meta_info = extract_meta_from_token(access_token)
|
||||
site_id = meta_info.get('metaSiteId') or site_id
|
||||
except Exception:
|
||||
pass
|
||||
try:
|
||||
@@ -363,7 +406,7 @@ async def handle_oauth_callback(request: WixAuthRequest, current_user: dict = De
|
||||
except Exception:
|
||||
pass
|
||||
try:
|
||||
permissions = wix_service.check_blog_permissions(access_token)
|
||||
permissions = wix_service.check_blog_permissions(access_token, site_id=site_id)
|
||||
except Exception as e:
|
||||
logger.warning(f"check_blog_permissions failed (non-fatal): {e}")
|
||||
else:
|
||||
@@ -425,10 +468,13 @@ async def handle_oauth_callback_get(code: str, state: Optional[str] = None, requ
|
||||
try:
|
||||
site_info = wix_service.get_site_info(tokens['access_token'])
|
||||
site_id = site_info.get('siteId') or site_info.get('site_id')
|
||||
if not site_id and site_info.get('_no_site'):
|
||||
meta_info = extract_meta_from_token(tokens['access_token'])
|
||||
site_id = meta_info.get('metaSiteId')
|
||||
except Exception as e:
|
||||
logger.warning(f"GET callback: get_site_info non-fatal: {e}")
|
||||
try:
|
||||
permissions = wix_service.check_blog_permissions(tokens['access_token'])
|
||||
permissions = wix_service.check_blog_permissions(tokens['access_token'], site_id=site_id)
|
||||
except Exception as e:
|
||||
logger.warning(f"GET callback: check_blog_permissions non-fatal: {e}")
|
||||
|
||||
@@ -499,17 +545,34 @@ async def get_connection_status(current_user: dict = Depends(get_current_user))
|
||||
try:
|
||||
token_info = _resolve_valid_wix_token(current_user)
|
||||
access_token = token_info["access_token"]
|
||||
site_id = token_info.get("site_id")
|
||||
|
||||
# Check site info — distinguish "no site" from "token expired"
|
||||
site_info = wix_service.get_site_info(access_token)
|
||||
permissions = wix_service.check_blog_permissions(access_token)
|
||||
if site_info.get("_auth_failed"):
|
||||
return {
|
||||
"connected": False,
|
||||
"has_permissions": False,
|
||||
"error": "Wix token expired — please reconnect",
|
||||
"reconnect_required": True
|
||||
}
|
||||
|
||||
# If get_site_info returned _no_site, try extracting metaSiteId from token
|
||||
if site_info.get("_no_site") and not site_id:
|
||||
meta_info = extract_meta_from_token(access_token)
|
||||
site_id = meta_info.get('metaSiteId')
|
||||
|
||||
permissions = wix_service.check_blog_permissions(access_token, site_id=site_id)
|
||||
return {
|
||||
"connected": True,
|
||||
"has_permissions": permissions.get("has_permissions", False),
|
||||
"site_info": site_info,
|
||||
"permissions": permissions
|
||||
"permissions": permissions,
|
||||
"site_id": site_id,
|
||||
}
|
||||
except HTTPException as e:
|
||||
if e.status_code == 401:
|
||||
return {"connected": False, "has_permissions": False, "error": "Wix account not connected"}
|
||||
return {"connected": False, "has_permissions": False, "error": "Wix account not connected", "reconnect_required": True}
|
||||
raise
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to check connection status: {e}")
|
||||
@@ -557,6 +620,9 @@ async def publish_to_wix(request: WixPublishRequest, current_user: dict = Depend
|
||||
access_token = token_info["access_token"]
|
||||
if not site_id:
|
||||
site_id = token_info.get("site_id")
|
||||
if not site_id:
|
||||
meta_info = extract_meta_from_token(access_token)
|
||||
site_id = meta_info.get('metaSiteId')
|
||||
logger.info(f"Wix publish: using backend DB token for user {_get_current_user_id(current_user)[:8]}...")
|
||||
except HTTPException:
|
||||
access_token = None
|
||||
@@ -641,12 +707,14 @@ async def publish_to_wix(request: WixPublishRequest, current_user: dict = Depend
|
||||
post_url = raw_url
|
||||
else:
|
||||
post_url = None
|
||||
publish_warnings = result.get("_warnings", [])
|
||||
all_warnings = [w for w in [content_warning] + publish_warnings if w]
|
||||
return {
|
||||
"success": True,
|
||||
"post_id": str(post.get("id", "")),
|
||||
"url": post_url,
|
||||
"publish_state": "PUBLISHED" if request.publish else "DRAFT",
|
||||
**({"warning": content_warning} if content_warning else {}),
|
||||
**({"warning": " | ".join(all_warnings)} if all_warnings else {}),
|
||||
}
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to publish to Wix: {e}")
|
||||
@@ -930,11 +998,13 @@ async def test_publish_real(payload: Dict[str, Any], _: Dict[str, Any] = Depends
|
||||
seo_metadata=seo_metadata,
|
||||
)
|
||||
|
||||
publish_warnings = result.get("_warnings", [])
|
||||
return {
|
||||
"success": True,
|
||||
"post_id": (result.get("draftPost") or result.get("post") or {}).get("id"),
|
||||
"url": (result.get("draftPost") or result.get("post") or {}).get("url"),
|
||||
"message": "Blog post published to Wix",
|
||||
**({"warning": " | ".join(publish_warnings)} if publish_warnings else {}),
|
||||
}
|
||||
except HTTPException:
|
||||
raise
|
||||
|
||||
Reference in New Issue
Block a user