from flask import Blueprint, request, jsonify, current_app from flask_jwt_extended import jwt_required, get_jwt_identity, verify_jwt_in_request from backend.services.content_service import ContentService from backend.services.linkedin_service import LinkedInService import uuid posts_bp = Blueprint('posts', __name__) def verify_jwt_with_cookies(): """Verify JWT token, checking both headers and cookies.""" try: # First try standard verification verify_jwt_in_request() except Exception as e: # If that fails, check if we have a token from cookies from flask import g if hasattr(g, 'jwt_token_from_cookie'): # We have a token from cookies, but verify_jwt_in_request failed # This might happen if the token is invalid or expired current_app.logger.info(f"[AUTH] JWT verification failed even with cookie token: {str(e)}") raise e else: # No token from cookies either, re-raise the original exception current_app.logger.info(f"[AUTH] JWT verification failed with no cookie token: {str(e)}") raise e def safe_log_message(message): """Safely log messages containing Unicode characters.""" try: # Try to encode as UTF-8 first, then decode with error handling if isinstance(message, str): # For strings, try to encode and decode safely encoded = message.encode('utf-8', errors='replace') safe_message = encoded.decode('utf-8', errors='replace') else: # For non-strings, convert to string first safe_message = str(message) # Log to app logger instead of print current_app.logger.debug(safe_message) except Exception as e: # Ultimate fallback - log the error current_app.logger.error(f"Failed to log message: {str(e)}") @posts_bp.route('/', methods=['OPTIONS']) @posts_bp.route('', methods=['OPTIONS']) def handle_options(): """Handle OPTIONS requests for preflight CORS checks.""" return '', 200 @posts_bp.route('/', methods=['GET']) @posts_bp.route('', methods=['GET']) def get_posts(): """ Get all posts for the current user. Query Parameters: published (bool): Filter by published status Returns: JSON: List of posts """ try: # Verify JWT with cookie support verify_jwt_with_cookies() user_id = get_jwt_identity() published = request.args.get('published', type=bool) # Check if Supabase client is initialized if not hasattr(current_app, 'supabase') or current_app.supabase is None: return jsonify({ 'success': False, 'message': 'Database connection not initialized' }), 500 # Build query query = ( current_app.supabase .table("Post_content") .select("*, Social_network(id_utilisateur)") ) # Apply published filter if specified if published is not None: query = query.eq("is_published", published) response = query.execute() # Filter posts for the current user user_posts = [ post for post in response.data if post.get('Social_network', {}).get('id_utilisateur') == user_id ] if response.data else [] return jsonify({ 'success': True, 'posts': user_posts }), 200 except Exception as e: error_message = str(e) safe_log_message(f"Get posts error: {error_message}") return jsonify({ 'success': False, 'message': 'An error occurred while fetching posts' }), 500 def _generate_post_task(user_id, job_id, job_store, hugging_key): """ Background task to generate post content. Args: user_id (str): User ID for personalization job_id (str): Job ID to update status in job store job_store (dict): Job store dictionary hugging_key (str): Hugging Face API key """ try: safe_log_message(f"[POST GENERATION] Starting post generation for user {user_id} with job {job_id}") # Update job status to processing job_store[job_id] = { 'status': 'processing', 'result': None, 'error': None } safe_log_message(f"[POST GENERATION] Job {job_id} status set to processing") # Generate content using content service # Pass the Hugging Face key directly to the service safe_log_message(f"[POST GENERATION] Initializing ContentService with Hugging Face key") content_service = ContentService(hugging_key=hugging_key) safe_log_message(f"[POST GENERATION] Calling generate_post_content for user {user_id}") generated_result = content_service.generate_post_content(user_id) safe_log_message(f"[POST GENERATION] Content generation completed successfully") # Handle the case where generated_result might be a tuple (content, image_data) # image_data could be bytes (from base64) or a string (URL) if isinstance(generated_result, tuple): generated_content, image_data = generated_result safe_log_message(f"[POST GENERATION] Generated content is a tuple. Content length: {len(generated_content) if generated_content else 0}, Image data: {'Present' if image_data else 'None'}") else: generated_content = generated_result image_data = None safe_log_message(f"[POST GENERATION] Generated content is a single value. Content length: {len(generated_content) if generated_content else 0}") # Log the actual content (first 100 chars) for debugging content_preview = generated_content[:100] if generated_content else "None" safe_log_message(f"[POST GENERATION] Generated content preview: {content_preview}") # Update job status to completed with result job_store[job_id] = { 'status': 'completed', 'result': { 'content': generated_content, 'image_data': image_data # This could be bytes or a URL string }, 'error': None } safe_log_message(f"[POST GENERATION] Job {job_id} status set to completed") except Exception as e: error_message = str(e) safe_log_message(f"[POST GENERATION] ERROR in background task: {error_message}") import traceback safe_log_message(f"[POST GENERATION] Traceback: {traceback.format_exc()}") # Update job status to failed with error job_store[job_id] = { 'status': 'failed', 'result': None, 'error': error_message } safe_log_message(f"[POST GENERATION] Job {job_id} status set to failed") @posts_bp.route('/generate', methods=['POST']) def generate_post(): """ Generate a new post using AI asynchronously. Request Body: user_id (str): User ID (optional, defaults to current user) Returns: JSON: Job ID for polling """ try: # Verify JWT with cookie support verify_jwt_with_cookies() safe_log_message("[POST GENERATION] Received request to generate post") current_user_id = get_jwt_identity() safe_log_message(f"[POST GENERATION] Current user ID: {current_user_id}") data = request.get_json() safe_log_message(f"[POST GENERATION] Request data: {data}") # Use provided user_id or default to current user user_id = data.get('user_id', current_user_id) safe_log_message(f"[POST GENERATION] Using user ID: {user_id}") # Verify user authorization (can only generate for self unless admin) if user_id != current_user_id: safe_log_message(f"[POST GENERATION] Unauthorized access attempt. Current user: {current_user_id}, Requested user: {user_id}") return jsonify({ 'success': False, 'message': 'Unauthorized to generate posts for other users' }), 403 # Create a job ID job_id = str(uuid.uuid4()) safe_log_message(f"[POST GENERATION] Created job ID: {job_id}") # Initialize job status current_app.job_store[job_id] = { 'status': 'pending', 'result': None, 'error': None } safe_log_message(f"[POST GENERATION] Job {job_id} initialized with pending status") # Get Hugging Face key hugging_key = current_app.config['HUGGING_KEY'] safe_log_message(f"[POST GENERATION] Retrieved Hugging Face key (length: {len(hugging_key) if hugging_key else 0})") # Submit the background task, passing all necessary data safe_log_message(f"[POST GENERATION] Submitting background task for job {job_id}") current_app.executor.submit(_generate_post_task, user_id, job_id, current_app.job_store, hugging_key) safe_log_message(f"[POST GENERATION] Background task submitted for job {job_id}") # Return job ID immediately safe_log_message(f"[POST GENERATION] Returning job ID {job_id} to client") return jsonify({ 'success': True, 'job_id': job_id, 'message': 'Post generation started' }), 202 # 202 Accepted except Exception as e: error_message = str(e) safe_log_message(f"[POST GENERATION] ERROR in generate_post: {error_message}") import traceback safe_log_message(f"[POST GENERATION] Traceback: {traceback.format_exc()}") return jsonify({ 'success': False, 'message': f'An error occurred while starting post generation: {error_message}' }), 500 @posts_bp.route('/jobs/', methods=['GET']) def get_job_status(job_id): """ Get the status of a post generation job. Path Parameters: job_id (str): Job ID Returns: JSON: Job status and result if completed """ try: # Verify JWT with cookie support verify_jwt_with_cookies() # Log the current user current_user = get_jwt_identity() safe_log_message(f"[JOB STATUS] Requesting status for job {job_id} by user {current_user}") # Check if we have a token from cookies from flask import g if hasattr(g, 'jwt_token_from_cookie'): safe_log_message(f"[JOB STATUS] Using JWT token from cookie for user {current_user}") # Get job from store job = current_app.job_store.get(job_id) if not job: safe_log_message(f"[JOB STATUS] Job {job_id} not found") return jsonify({ 'success': False, 'message': 'Job not found' }), 404 safe_log_message(f"[JOB STATUS] Job {job_id} found with status: {job['status']}") # Prepare response response_data = { 'success': True, 'job_id': job_id, 'status': job['status'] } # Include result or error if available if job['status'] == 'completed': safe_log_message(f"[JOB STATUS] Job {job_id} is completed, preparing result") # Handle the new structure of the result if isinstance(job['result'], dict) and 'content' in job['result']: response_data['content'] = job['result']['content'] # Handle image_data which could be bytes or a URL string image_data = job['result'].get('image_data') if isinstance(image_data, bytes): # If it's bytes, we can't send it directly in JSON # For now, we'll set it to None and let the frontend know # In a future update, we might save it to storage and return a URL response_data['image_url'] = None response_data['has_image_data'] = True # Flag to indicate image data exists safe_log_message(f"[JOB STATUS] Job {job_id} has image data (bytes)") else: # If it's a string, assume it's a URL response_data['image_url'] = image_data response_data['has_image_data'] = False safe_log_message(f"[JOB STATUS] Job {job_id} has image data (URL or None): {image_data}") else: response_data['content'] = job['result'] response_data['image_url'] = None response_data['has_image_data'] = False safe_log_message(f"[JOB STATUS] Job {job_id} has simple result format") elif job['status'] == 'failed': response_data['error'] = job['error'] safe_log_message(f"[JOB STATUS] Job {job_id} failed with error: {job['error']}") elif job['status'] == 'processing': safe_log_message(f"[JOB STATUS] Job {job_id} is still processing") safe_log_message(f"[JOB STATUS] Returning response for job {job_id}") return jsonify(response_data), 200 except Exception as e: error_message = str(e) safe_log_message(f"[JOB STATUS] ERROR in get_job_status: {error_message}") import traceback safe_log_message(f"[JOB STATUS] Traceback: {traceback.format_exc()}") return jsonify({ 'success': False, 'message': f'An error occurred while fetching job status: {error_message}' }), 500 @posts_bp.route('/publish-direct', methods=['OPTIONS']) def handle_publish_direct_options(): """Handle OPTIONS requests for preflight CORS checks for publish direct route.""" return '', 200 @posts_bp.route('/publish-direct', methods=['POST']) def publish_post_direct(): """ Publish a post directly to social media and save to database. Request Body: social_account_id (str): Social account ID text_content (str): Post text content image_content_url (str, optional): Image URL scheduled_at (str, optional): Scheduled time in ISO format Returns: JSON: Publish post result """ try: # Verify JWT with cookie support verify_jwt_with_cookies() user_id = get_jwt_identity() data = request.get_json() # Validate required fields social_account_id = data.get('social_account_id') text_content = data.get('text_content') if not social_account_id or not text_content: return jsonify({ 'success': False, 'message': 'social_account_id and text_content are required' }), 400 # Verify the social account belongs to the user account_response = ( current_app.supabase .table("Social_network") .select("id_utilisateur, token, sub") .eq("id", social_account_id) .execute() ) if not account_response.data: return jsonify({ 'success': False, 'message': 'Social account not found' }), 404 account = account_response.data[0] if account.get('id_utilisateur') != user_id: return jsonify({ 'success': False, 'message': 'Unauthorized to use this social account' }), 403 # Get account details access_token = account.get('token') user_sub = account.get('sub') if not access_token or not user_sub: return jsonify({ 'success': False, 'message': 'Social account not properly configured' }), 400 # Get optional fields image_data = data.get('image_content_url') # This could be bytes or a URL string # Handle image data - if it's bytes, we need to convert it for LinkedIn image_url_for_linkedin = None if image_data: if isinstance(image_data, bytes): # If it's bytes, we can't directly send it to LinkedIn # For now, we'll skip sending the image to LinkedIn # In a future update, we might save it to storage and get a URL current_app.logger.warning("Image data is in bytes format, skipping LinkedIn upload for now") else: # If it's a string, assume it's a URL image_url_for_linkedin = image_data # Publish to LinkedIn linkedin_service = LinkedInService() publish_response = linkedin_service.publish_post( access_token, user_sub, text_content, image_url_for_linkedin ) # Save to database as published post_data = { 'id_social': social_account_id, 'Text_content': text_content, 'is_published': True } # Add optional fields if provided if image_data: post_data['image_content_url'] = image_data if 'scheduled_at' in data: post_data['scheduled_at'] = data['scheduled_at'] # Insert post into database response = ( current_app.supabase .table("Post_content") .insert(post_data) .execute() ) if response.data: return jsonify({ 'success': True, 'message': 'Post published and saved successfully', 'post': response.data[0], 'linkedin_response': publish_response }), 201 else: return jsonify({ 'success': False, 'message': 'Failed to save post to database' }), 500 except Exception as e: error_message = str(e) safe_log_message(f"[Post] Publish post directly error: {error_message}") return jsonify({ 'success': False, 'message': f'An error occurred while publishing post: {error_message}' }), 500 @posts_bp.route('/', methods=['OPTIONS']) def handle_post_options(post_id): """Handle OPTIONS requests for preflight CORS checks for specific post.""" return '', 200 @posts_bp.route('/', methods=['POST']) @posts_bp.route('', methods=['POST']) def create_post(): """ Create a new post. Request Body: social_account_id (str): Social account ID text_content (str): Post text content image_content_url (str, optional): Image URL scheduled_at (str, optional): Scheduled time in ISO format is_published (bool, optional): Whether the post is published (defaults to True) Returns: JSON: Created post data """ try: # Verify JWT with cookie support verify_jwt_with_cookies() user_id = get_jwt_identity() data = request.get_json() # Validate required fields social_account_id = data.get('social_account_id') text_content = data.get('text_content') if not social_account_id or not text_content: return jsonify({ 'success': False, 'message': 'social_account_id and text_content are required' }), 400 # Verify the social account belongs to the user account_response = ( current_app.supabase .table("Social_network") .select("id_utilisateur") .eq("id", social_account_id) .execute() ) if not account_response.data: return jsonify({ 'success': False, 'message': 'Social account not found' }), 404 if account_response.data[0].get('id_utilisateur') != user_id: return jsonify({ 'success': False, 'message': 'Unauthorized to use this social account' }), 403 # Prepare post data - always mark as published post_data = { 'id_social': social_account_id, 'Text_content': text_content, 'is_published': data.get('is_published', True) # Default to True } # Handle image data - could be bytes or a URL string image_data = data.get('image_content_url') # Add optional fields if provided if image_data is not None: post_data['image_content_url'] = image_data if 'scheduled_at' in data: post_data['scheduled_at'] = data['scheduled_at'] # Insert post into database response = ( current_app.supabase .table("Post_content") .insert(post_data) .execute() ) if response.data: return jsonify({ 'success': True, 'post': response.data[0] }), 201 else: return jsonify({ 'success': False, 'message': 'Failed to create post' }), 500 except Exception as e: error_message = str(e) safe_log_message(f"[Post] Create post error: {error_message}") return jsonify({ 'success': False, 'message': f'An error occurred while creating post: {error_message}' }), 500 @posts_bp.route('/', methods=['DELETE']) def delete_post(post_id): """ Delete a post. Path Parameters: post_id (str): Post ID Returns: JSON: Delete post result """ try: # Verify JWT with cookie support verify_jwt_with_cookies() user_id = get_jwt_identity() # Verify the post belongs to the user response = ( current_app.supabase .table("Post_content") .select("Social_network(id_utilisateur)") .eq("id", post_id) .execute() ) if not response.data: return jsonify({ 'success': False, 'message': 'Post not found' }), 404 post = response.data[0] if post.get('Social_network', {}).get('id_utilisateur') != user_id: return jsonify({ 'success': False, 'message': 'Unauthorized to delete this post' }), 403 # Delete post from Supabase delete_response = ( current_app.supabase .table("Post_content") .delete() .eq("id", post_id) .execute() ) if delete_response.data: return jsonify({ 'success': True, 'message': 'Post deleted successfully' }), 200 else: return jsonify({ 'success': False, 'message': 'Failed to delete post' }), 500 except Exception as e: error_message = str(e) safe_log_message(f"Delete post error: {error_message}") return jsonify({ 'success': False, 'message': 'An error occurred while deleting post' }), 500