185 lines
6.8 KiB
Python
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}") |