""" WordPress Webhook Handlers Receives status updates and sync events from WordPress """ from rest_framework.decorators import api_view, permission_classes, throttle_classes from rest_framework.permissions import AllowAny from rest_framework.throttling import BaseThrottle from rest_framework.response import Response from rest_framework import status as http_status from django.utils import timezone import logging from igny8_core.api.response import success_response, error_response from igny8_core.business.content.models import Content from igny8_core.business.integration.models import SiteIntegration, SyncEvent logger = logging.getLogger(__name__) webhook_logger = logging.getLogger('webhooks') class NoThrottle(BaseThrottle): """No throttle for webhooks""" def allow_request(self, request, view): return True @api_view(['POST']) @permission_classes([AllowAny]) # Webhook authentication handled inside @throttle_classes([NoThrottle]) def wordpress_status_webhook(request): """ Receive WordPress post status updates via webhook POST /api/v1/integration/webhooks/wordpress/status/ Headers: - X-IGNY8-API-KEY: Body: { "post_id": 123, # WordPress post ID "content_id": 456, # IGNY8 content ID "post_status": "publish", # WordPress status "post_url": "https://example.com/post/...", "post_title": "Post Title", "site_url": "https://example.com" } """ try: webhook_logger.info("="*80) webhook_logger.info("📥 WORDPRESS STATUS WEBHOOK RECEIVED") webhook_logger.info(f" Headers: {dict(request.headers)}") webhook_logger.info(f" Body: {request.data}") webhook_logger.info("="*80) # Validate API key api_key = request.headers.get('X-IGNY8-API-KEY') or request.headers.get('Authorization', '').replace('Bearer ', '') if not api_key: webhook_logger.error(" ❌ Missing API key in request headers") return error_response( error='Missing API key', status_code=http_status.HTTP_401_UNAUTHORIZED, request=request ) webhook_logger.info(f" ✅ API key present: ***{api_key[-4:]}") # Get webhook data data = request.data content_id = data.get('content_id') post_id = data.get('post_id') post_status = data.get('post_status') post_url = data.get('post_url') site_url = data.get('site_url') webhook_logger.info(f"STEP 1: Parsing webhook data...") webhook_logger.info(f" - Content ID: {content_id}") webhook_logger.info(f" - Post ID: {post_id}") webhook_logger.info(f" - Post Status: {post_status}") webhook_logger.info(f" - Post URL: {post_url}") webhook_logger.info(f" - Site URL: {site_url}") # Validate required fields if not content_id or not post_id or not post_status: webhook_logger.error(" ❌ Missing required fields") return error_response( error='Missing required fields: content_id, post_id, post_status', status_code=http_status.HTTP_400_BAD_REQUEST, request=request ) # Find content try: content = Content.objects.get(id=content_id) except Content.DoesNotExist: logger.error(f"[wordpress_status_webhook] Content {content_id} not found") return error_response( error=f'Content {content_id} not found', status_code=http_status.HTTP_404_NOT_FOUND, request=request ) # Find site integration by site_url and verify API key if site_url: integration = SiteIntegration.objects.filter( site=content.site, platform='wordpress', config_json__site_url=site_url ).first() else: # Fallback: find any active WordPress integration for this site integration = SiteIntegration.objects.filter( site=content.site, platform='wordpress', is_active=True ).first() if not integration: logger.error(f"[wordpress_status_webhook] No WordPress integration found for site {content.site.name}") return error_response( error='WordPress integration not found for this site', status_code=http_status.HTTP_404_NOT_FOUND, request=request ) # Verify API key matches integration stored_api_key = integration.credentials_json.get('api_key') if not stored_api_key or stored_api_key != api_key: logger.error(f"[wordpress_status_webhook] Invalid API key for integration {integration.id}") return error_response( error='Invalid API key', status_code=http_status.HTTP_401_UNAUTHORIZED, request=request ) # Map WordPress status to IGNY8 status status_map = { 'publish': 'published', 'draft': 'draft', 'pending': 'review', 'private': 'published', 'trash': 'draft', 'future': 'review', } igny8_status = status_map.get(post_status, 'review') # Update content old_status = content.status old_wp_status = content.metadata.get('wordpress_status') if content.metadata else None content.external_id = str(post_id) if post_url: content.external_url = post_url # FIXED: Always update status when WordPress status differs if content.status != igny8_status: content.status = igny8_status logger.info(f"[wordpress_status_webhook] Status updated: {old_status} → {content.status}") # Update WordPress status in metadata if not content.metadata: content.metadata = {} content.metadata['wordpress_status'] = post_status content.metadata['last_wp_sync'] = timezone.now().isoformat() content.save(update_fields=['external_id', 'external_url', 'status', 'metadata', 'updated_at']) logger.info(f"[wordpress_status_webhook] Updated content {content_id}:") logger.info(f" - Status: {old_status} → {content.status}") logger.info(f" - WP Status: {old_wp_status} → {post_status}") logger.info(f" - External ID: {content.external_id}") # Log sync event SyncEvent.objects.create( integration=integration, site=content.site, account=content.account, event_type='webhook', action='status_update', description=f"WordPress status updated: '{content.title}' is now {post_status}", success=True, content_id=content.id, external_id=str(post_id), details={ 'old_status': old_status, 'new_status': content.status, 'old_wp_status': old_wp_status, 'new_wp_status': post_status, 'post_url': post_url, } ) return success_response( data={ 'content_id': content.id, 'status': content.status, 'wordpress_status': post_status, 'external_id': content.external_id, 'external_url': content.external_url, }, message='Content status updated successfully', request=request ) except Exception as e: logger.error(f"[wordpress_status_webhook] Error processing webhook: {str(e)}", exc_info=True) return error_response( error=f'Failed to process webhook: {str(e)}', status_code=http_status.HTTP_500_INTERNAL_SERVER_ERROR, request=request ) @api_view(['POST']) @permission_classes([AllowAny]) @throttle_classes([NoThrottle]) def wordpress_metadata_webhook(request): """ Receive WordPress metadata updates via webhook POST /api/v1/integration/webhooks/wordpress/metadata/ Headers: - X-IGNY8-API-KEY: Body: { "post_id": 123, "content_id": 456, "site_url": "https://example.com", "metadata": { "categories": [...], "tags": [...], "author": {...}, "modified_date": "2025-11-30T..." } } """ try: # Validate API key api_key = request.headers.get('X-IGNY8-API-KEY') or request.headers.get('Authorization', '').replace('Bearer ', '') if not api_key: return error_response( error='Missing API key', status_code=http_status.HTTP_401_UNAUTHORIZED, request=request ) # Get webhook data data = request.data content_id = data.get('content_id') post_id = data.get('post_id') site_url = data.get('site_url') metadata = data.get('metadata', {}) logger.info(f"[wordpress_metadata_webhook] Received webhook: content_id={content_id}, post_id={post_id}") # Validate required fields if not content_id or not post_id: return error_response( error='Missing required fields: content_id, post_id', status_code=http_status.HTTP_400_BAD_REQUEST, request=request ) # Find content try: content = Content.objects.get(id=content_id) except Content.DoesNotExist: return error_response( error=f'Content {content_id} not found', status_code=http_status.HTTP_404_NOT_FOUND, request=request ) # Find integration and verify API key if site_url: integration = SiteIntegration.objects.filter( site=content.site, platform='wordpress', config_json__site_url=site_url ).first() else: integration = SiteIntegration.objects.filter( site=content.site, platform='wordpress', is_active=True ).first() if not integration: return error_response( error='WordPress integration not found', status_code=http_status.HTTP_404_NOT_FOUND, request=request ) # Verify API key stored_api_key = integration.credentials_json.get('api_key') if not stored_api_key or stored_api_key != api_key: return error_response( error='Invalid API key', status_code=http_status.HTTP_401_UNAUTHORIZED, request=request ) # Update content metadata if not content.metadata: content.metadata = {} content.metadata['wp_metadata'] = metadata content.metadata['last_metadata_sync'] = timezone.now().isoformat() content.save(update_fields=['metadata', 'updated_at']) # Log sync event SyncEvent.objects.create( integration=integration, site=content.site, account=content.account, event_type='metadata_sync', action='metadata_update', description=f"WordPress metadata synced for '{content.title}'", success=True, content_id=content.id, external_id=str(post_id), details=metadata ) logger.info(f"[wordpress_metadata_webhook] Updated metadata for content {content_id}") return success_response( data={ 'content_id': content.id, 'metadata_updated': True, }, message='Metadata updated successfully', request=request ) except Exception as e: logger.error(f"[wordpress_metadata_webhook] Error: {str(e)}", exc_info=True) return error_response( error=f'Failed to process webhook: {str(e)}', status_code=http_status.HTTP_500_INTERNAL_SERVER_ERROR, request=request )