|
import codecs |
|
import uuid |
|
from flask import Blueprint, request, jsonify, current_app |
|
from flask_jwt_extended import jwt_required, get_jwt_identity |
|
from backend.services.content_service import ContentService |
|
from backend.services.linkedin_service import LinkedInService |
|
|
|
posts_bp = Blueprint('posts', __name__) |
|
|
|
def safe_log_message(message): |
|
"""Safely log messages containing Unicode characters.""" |
|
try: |
|
|
|
if isinstance(message, str): |
|
|
|
encoded = message.encode('utf-8', errors='replace') |
|
safe_message = encoded.decode('utf-8', errors='replace') |
|
else: |
|
|
|
safe_message = str(message) |
|
|
|
|
|
current_app.logger.debug(safe_message) |
|
except Exception as e: |
|
|
|
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']) |
|
@jwt_required() |
|
def get_posts(): |
|
""" |
|
Get all posts for the current user. |
|
|
|
Query Parameters: |
|
published (bool): Filter by published status |
|
|
|
Returns: |
|
JSON: List of posts |
|
""" |
|
try: |
|
user_id = get_jwt_identity() |
|
published = request.args.get('published', type=bool) |
|
|
|
|
|
if not hasattr(current_app, 'supabase') or current_app.supabase is None: |
|
|
|
response_data = jsonify({ |
|
'success': False, |
|
'message': 'Database connection not initialized' |
|
}) |
|
response_data.headers.add('Access-Control-Allow-Origin', 'http://localhost:3000') |
|
response_data.headers.add('Access-Control-Allow-Credentials', 'true') |
|
return response_data, 500 |
|
|
|
|
|
query = ( |
|
current_app.supabase |
|
.table("Post_content") |
|
.select("*, Social_network(id_utilisateur)") |
|
) |
|
|
|
|
|
if published is not None: |
|
query = query.eq("is_published", published) |
|
|
|
response = query.execute() |
|
|
|
|
|
user_posts = [ |
|
post for post in response.data |
|
if post.get('Social_network', {}).get('id_utilisateur') == user_id |
|
] if response.data else [] |
|
|
|
|
|
response_data = jsonify({ |
|
'success': True, |
|
'posts': user_posts |
|
}) |
|
response_data.headers.add('Access-Control-Allow-Origin', 'http://localhost:3000') |
|
response_data.headers.add('Access-Control-Allow-Credentials', 'true') |
|
return response_data, 200 |
|
|
|
except Exception as e: |
|
error_message = str(e) |
|
safe_log_message(f"Get posts error: {error_message}") |
|
|
|
response_data = jsonify({ |
|
'success': False, |
|
'message': 'An error occurred while fetching posts' |
|
}) |
|
response_data.headers.add('Access-Control-Allow-Origin', 'http://localhost:3000') |
|
response_data.headers.add('Access-Control-Allow-Credentials', 'true') |
|
return response_data, 500 |
|
|
|
|
|
response_data = jsonify({ |
|
'success': True, |
|
'posts': user_posts |
|
}) |
|
response_data.headers.add('Access-Control-Allow-Origin', 'http://localhost:3000') |
|
response_data.headers.add('Access-Control-Allow-Credentials', 'true') |
|
return response_data, 200 |
|
|
|
except Exception as e: |
|
error_message = str(e) |
|
safe_log_message(f"Get posts error: {error_message}") |
|
|
|
response_data = jsonify({ |
|
'success': False, |
|
'message': 'An error occurred while fetching posts' |
|
}) |
|
response_data.headers.add('Access-Control-Allow-Origin', 'http://localhost:3000') |
|
response_data.headers.add('Access-Control-Allow-Credentials', 'true') |
|
return response_data, 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: |
|
|
|
job_store[job_id] = { |
|
'status': 'processing', |
|
'result': None, |
|
'error': None |
|
} |
|
|
|
|
|
|
|
content_service = ContentService(hugging_key=hugging_key) |
|
generated_content = content_service.generate_post_content(user_id) |
|
|
|
|
|
job_store[job_id] = { |
|
'status': 'completed', |
|
'result': generated_content, |
|
'error': None |
|
} |
|
|
|
except Exception as e: |
|
error_message = str(e) |
|
safe_log_message(f"Generate post background task error: {error_message}") |
|
|
|
job_store[job_id] = { |
|
'status': 'failed', |
|
'result': None, |
|
'error': error_message |
|
} |
|
|
|
@posts_bp.route('/generate', methods=['POST']) |
|
@jwt_required() |
|
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: |
|
current_user_id = get_jwt_identity() |
|
data = request.get_json() |
|
|
|
|
|
user_id = data.get('user_id', current_user_id) |
|
|
|
|
|
if user_id != current_user_id: |
|
return jsonify({ |
|
'success': False, |
|
'message': 'Unauthorized to generate posts for other users' |
|
}), 403 |
|
|
|
|
|
job_id = str(uuid.uuid4()) |
|
|
|
|
|
current_app.job_store[job_id] = { |
|
'status': 'pending', |
|
'result': None, |
|
'error': None |
|
} |
|
|
|
|
|
hugging_key = current_app.config['HUGGING_KEY'] |
|
|
|
|
|
current_app.executor.submit(_generate_post_task, user_id, job_id, current_app.job_store, hugging_key) |
|
|
|
|
|
return jsonify({ |
|
'success': True, |
|
'job_id': job_id, |
|
'message': 'Post generation started' |
|
}), 202 |
|
|
|
except Exception as e: |
|
error_message = str(e) |
|
safe_log_message(f"Generate post error: {error_message}") |
|
return jsonify({ |
|
'success': False, |
|
'message': f'An error occurred while starting post generation: {error_message}' |
|
}), 500 |
|
|
|
@posts_bp.route('/jobs/<job_id>', methods=['GET']) |
|
@jwt_required() |
|
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: |
|
|
|
job = current_app.job_store.get(job_id) |
|
|
|
if not job: |
|
return jsonify({ |
|
'success': False, |
|
'message': 'Job not found' |
|
}), 404 |
|
|
|
|
|
response_data = { |
|
'success': True, |
|
'job_id': job_id, |
|
'status': job['status'] |
|
} |
|
|
|
|
|
if job['status'] == 'completed': |
|
response_data['content'] = job['result'] |
|
elif job['status'] == 'failed': |
|
response_data['error'] = job['error'] |
|
|
|
return jsonify(response_data), 200 |
|
|
|
except Exception as e: |
|
error_message = str(e) |
|
safe_log_message(f"Get job status error: {error_message}") |
|
return jsonify({ |
|
'success': False, |
|
'message': f'An error occurred while fetching job status: {error_message}' |
|
}), 500 |
|
|
|
@posts_bp.route('/', methods=['OPTIONS']) |
|
@posts_bp.route('', methods=['OPTIONS']) |
|
def handle_create_options(): |
|
"""Handle OPTIONS requests for preflight CORS checks for create post route.""" |
|
return '', 200 |
|
|
|
@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']) |
|
@jwt_required() |
|
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: |
|
user_id = get_jwt_identity() |
|
data = request.get_json() |
|
|
|
|
|
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 |
|
|
|
|
|
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 |
|
|
|
|
|
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 |
|
|
|
|
|
image_url = data.get('image_content_url') |
|
|
|
|
|
linkedin_service = LinkedInService() |
|
publish_response = linkedin_service.publish_post( |
|
access_token, user_sub, text_content, image_url |
|
) |
|
|
|
|
|
post_data = { |
|
'id_social': social_account_id, |
|
'Text_content': text_content, |
|
'is_published': True |
|
} |
|
|
|
|
|
if image_url: |
|
post_data['image_content_url'] = image_url |
|
|
|
if 'scheduled_at' in data: |
|
post_data['scheduled_at'] = data['scheduled_at'] |
|
|
|
|
|
response = ( |
|
current_app.supabase |
|
.table("Post_content") |
|
.insert(post_data) |
|
.execute() |
|
) |
|
|
|
if response.data: |
|
|
|
response_data = jsonify({ |
|
'success': True, |
|
'message': 'Post published and saved successfully', |
|
'post': response.data[0], |
|
'linkedin_response': publish_response |
|
}) |
|
response_data.headers.add('Access-Control-Allow-Origin', 'http://localhost:3000') |
|
response_data.headers.add('Access-Control-Allow-Credentials', 'true') |
|
return response_data, 201 |
|
else: |
|
|
|
response_data = jsonify({ |
|
'success': False, |
|
'message': 'Failed to save post to database' |
|
}) |
|
response_data.headers.add('Access-Control-Allow-Origin', 'http://localhost:3000') |
|
response_data.headers.add('Access-Control-Allow-Credentials', 'true') |
|
return response_data, 500 |
|
|
|
except Exception as e: |
|
error_message = str(e) |
|
safe_log_message(f"[Post] Publish post directly error: {error_message}") |
|
|
|
response_data = jsonify({ |
|
'success': False, |
|
'message': f'An error occurred while publishing post: {error_message}' |
|
}) |
|
response_data.headers.add('Access-Control-Allow-Origin', 'http://localhost:3000') |
|
response_data.headers.add('Access-Control-Allow-Credentials', 'true') |
|
return response_data, 500 |
|
|
|
@posts_bp.route('/<post_id>', 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']) |
|
@jwt_required() |
|
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: |
|
user_id = get_jwt_identity() |
|
data = request.get_json() |
|
|
|
|
|
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 |
|
|
|
|
|
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 |
|
|
|
|
|
post_data = { |
|
'id_social': social_account_id, |
|
'Text_content': text_content, |
|
'is_published': data.get('is_published', True) |
|
} |
|
|
|
|
|
if 'image_content_url' in data: |
|
post_data['image_content_url'] = data['image_content_url'] |
|
|
|
if 'scheduled_at' in data: |
|
post_data['scheduled_at'] = data['scheduled_at'] |
|
|
|
|
|
response = ( |
|
current_app.supabase |
|
.table("Post_content") |
|
.insert(post_data) |
|
.execute() |
|
) |
|
|
|
if response.data: |
|
|
|
response_data = jsonify({ |
|
'success': True, |
|
'post': response.data[0] |
|
}) |
|
response_data.headers.add('Access-Control-Allow-Origin', 'http://localhost:3000') |
|
response_data.headers.add('Access-Control-Allow-Credentials', 'true') |
|
return response_data, 201 |
|
else: |
|
|
|
response_data = jsonify({ |
|
'success': False, |
|
'message': 'Failed to create post' |
|
}) |
|
response_data.headers.add('Access-Control-Allow-Origin', 'http://localhost:3000') |
|
response_data.headers.add('Access-Control-Allow-Credentials', 'true') |
|
return response_data, 500 |
|
|
|
except Exception as e: |
|
error_message = str(e) |
|
safe_log_message(f"[Post] Create post error: {error_message}") |
|
|
|
response_data = jsonify({ |
|
'success': False, |
|
'message': f'An error occurred while creating post: {error_message}' |
|
}) |
|
response_data.headers.add('Access-Control-Allow-Origin', 'http://localhost:3000') |
|
response_data.headers.add('Access-Control-Allow-Credentials', 'true') |
|
return response_data, 500 |
|
|
|
@posts_bp.route('/<post_id>', methods=['DELETE']) |
|
@jwt_required() |
|
def delete_post(post_id): |
|
""" |
|
Delete a post. |
|
|
|
Path Parameters: |
|
post_id (str): Post ID |
|
|
|
Returns: |
|
JSON: Delete post result |
|
""" |
|
try: |
|
user_id = get_jwt_identity() |
|
|
|
|
|
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_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 |