2026-02-12 15:09:48 +03:00

185 lines
6.8 KiB
Python

# xcom/tasks.py
from celery import shared_task
from django.utils import timezone
from django.utils.dateparse import parse_datetime
from apps.social.models import SocialAccount, SocialContent, SocialComment
from apps.social.services.x import XService, XAPIError, XRateLimitError
import logging
logger = logging.getLogger(__name__)
def parse_x_timestamp(ts):
if not ts: return timezone.now()
try:
dt = parse_datetime(ts)
return dt if dt else timezone.now()
except:
return timezone.now()
@shared_task
def sync_all_accounts_periodic():
logger.info("Starting Periodic Polling for X (Twitter)...")
accounts = SocialAccount.objects.filter(platform_type='X', is_active=True)
for account in accounts:
try:
poll_new_replies_task.delay(account.id)
except Exception as e:
logger.error(f"Failed to trigger sync for account {account.id}: {e}")
@shared_task(bind=True, max_retries=6) # Allow retries for rate limits
def poll_new_replies_task(self, account_id):
"""
DELTA SYNC TASK with intelligent Rate Limit handling.
"""
try:
account = SocialAccount.objects.get(id=account_id, platform_type='X')
tweets = SocialContent.objects.filter(platform_type='X', account=account)
for tweet in tweets:
if not tweet.last_comment_sync_at:
continue # Skip unsynced tweets
try:
replies_data = XService.fetch_tweet_replies(
account,
tweet.content_id,
since_datetime=tweet.last_comment_sync_at,
owner_id=account.platform_id
)
except XRateLimitError as e:
# CRITICAL: Don't block. Retry later using ETA (Estimated Time of Arrival)
# Add a buffer of 60 seconds to ensure limit is reset
retry_time = timezone.make_aware(datetime.datetime.fromtimestamp(e.reset_at))
wait_seconds = (retry_time - timezone.now()).total_seconds() + 60
logger.warning(f"Rate limit hit. Retrying in {wait_seconds} seconds.")
raise self.retry(exc=XAPIError("Rate limit triggered"), countdown=wait_seconds)
if not replies_data:
continue
newest_timestamp_found = tweet.last_comment_sync_at
for r_data in replies_data:
# r_data['author'] is now populated by _attach_expansions
author = r_data.get('author')
if not author:
# Fallback if expansion failed (shouldn't happen)
continue
r_time = parse_x_timestamp(r_data['created_at'])
if r_time <= tweet.last_comment_sync_at:
continue
SocialComment.objects.update_or_create(
platform_type='X',
comment_id=r_data['id'],
defaults={
'account': account,
'content': tweet,
'text': r_data.get('text', ''),
'author_id': author.get('id'),
'author_name': author.get('username', 'Unknown'), # Now correctly fetched
'created_at': r_time,
'comment_data': r_data
}
)
if r_time > newest_timestamp_found:
newest_timestamp_found = r_time
if newest_timestamp_found > tweet.last_comment_sync_at:
tweet.last_comment_sync_at = newest_timestamp_found
tweet.save()
account.last_synced_at = timezone.now()
account.save()
except SocialAccount.DoesNotExist:
logger.error(f"Account {account_id} not found")
except XRateLimitError:
# Raise it again if we haven't retried enough times
raise
except Exception as e:
logger.error(f"Polling Error for account {account_id}: {e}")
@shared_task(bind=True)
def extract_all_replies_task(self, account_id):
"""
MANUAL FULL SYNC (Backfill)
"""
try:
account = SocialAccount.objects.get(id=account_id, platform_type='X')
tweets_data = XService.get_user_tweets(account)
if not tweets_data:
logger.info(f"No tweets found for account {account.name}")
return
for t_data in tweets_data:
tweet_id = t_data['id']
tweet, created = SocialContent.objects.get_or_create(
platform_type='X',
content_id=tweet_id,
defaults={
'account': account,
'text': t_data.get('text', ''),
'created_at': parse_x_timestamp(t_data['created_at']),
'content_data': t_data
}
)
# Rate limit handling for fetch inside backfill
try:
replies_data = XService.fetch_tweet_replies(
account,
tweet_id,
since_datetime=None,
owner_id=account.platform_id
)
except XRateLimitError as e:
# Retry backfill later
retry_time = timezone.make_aware(datetime.datetime.fromtimestamp(e.reset_at))
wait_seconds = (retry_time - timezone.now()).total_seconds() + 60
raise self.retry(exc=XAPIError("Rate limit triggered"), countdown=wait_seconds)
if not replies_data:
continue
for r_data in replies_data:
author = r_data.get('author')
if not author: continue
SocialComment.objects.update_or_create(
platform_type='X',
comment_id=r_data['id'],
defaults={
'account': account,
'content': tweet,
'text': r_data.get('text', ''),
'author_id': author.get('id'),
'author_name': author.get('username', 'Unknown'),
'created_at': parse_x_timestamp(r_data['created_at']),
'comment_data': r_data
}
)
if created:
tweet.last_comment_sync_at = tweet.created_at
tweet.save()
account.last_synced_at = timezone.now()
account.save()
except SocialAccount.DoesNotExist:
logger.error(f"Account {account_id} not found")
except XRateLimitError:
raise
except Exception as e:
logger.error(f"Backfill Error for account {account_id}: {e}")