Files
ALwrity/ToBeMigrated/database/twitter_service.py
2025-08-06 16:29:49 +05:30

766 lines
29 KiB
Python

"""
Twitter Database Service Layer
=============================
This module provides high-level service functions for managing Twitter data
in the database. It acts as an interface between the application and the
database models, providing convenient methods for common operations.
Key Features:
- User profile management and synchronization
- Tweet creation, updating, and analytics tracking
- Scheduled tweet management
- Analytics data aggregation and reporting
- Hashtag performance tracking
- Audience insights management
"""
import logging
from typing import Dict, List, Any, Optional, Tuple
from datetime import datetime, timedelta
from sqlalchemy.orm import Session
from sqlalchemy import func, desc, and_, or_
import json
from cryptography.fernet import Fernet
import os
from .twitter_models import (
TwitterUser, Tweet, ScheduledTweet, TwitterAnalytics, TweetAnalytics,
EngagementData, AudienceInsight, HashtagPerformance, ContentTemplate,
TwitterSettings, TwitterCredentials, TweetMetrics,
TwitterAccountType, TweetType, TweetStatus, EngagementType,
AnalyticsTimeframe, ContentCategory,
get_twitter_engine, init_twitter_db, get_twitter_session,
create_twitter_user, update_user_metrics, create_tweet_record,
update_tweet_metrics, calculate_virality_score, get_user_analytics_summary
)
# Configure logging
logger = logging.getLogger(__name__)
class TwitterDatabaseService:
"""
High-level service for managing Twitter data in the database.
"""
def __init__(self, db_url: str = "sqlite:///twitter_data.db", encryption_key: Optional[str] = None):
"""Initialize the Twitter database service."""
self.engine = get_twitter_engine(db_url)
self.encryption_key = encryption_key or self._get_or_create_encryption_key()
self.cipher = Fernet(self.encryption_key.encode() if isinstance(self.encryption_key, str) else self.encryption_key)
# Initialize database
init_twitter_db(self.engine)
logger.info("Twitter database service initialized")
def _get_or_create_encryption_key(self) -> str:
"""Get or create encryption key for sensitive data."""
key_file = "twitter_encryption.key"
if os.path.exists(key_file):
with open(key_file, 'rb') as f:
return f.read()
else:
key = Fernet.generate_key()
with open(key_file, 'wb') as f:
f.write(key)
return key
def _encrypt_credentials(self, credentials: TwitterCredentials) -> str:
"""Encrypt Twitter credentials for secure storage."""
credentials_json = json.dumps(credentials.to_dict())
encrypted = self.cipher.encrypt(credentials_json.encode())
return encrypted.decode()
def _decrypt_credentials(self, encrypted_credentials: str) -> TwitterCredentials:
"""Decrypt Twitter credentials from storage."""
try:
decrypted = self.cipher.decrypt(encrypted_credentials.encode())
credentials_dict = json.loads(decrypted.decode())
return TwitterCredentials.from_dict(credentials_dict)
except Exception as e:
logger.error(f"Failed to decrypt credentials: {e}")
return TwitterCredentials()
def get_session(self) -> Session:
"""Get a database session."""
return get_twitter_session(self.engine)
# --- USER MANAGEMENT ---
def create_or_update_user(self, user_data: Dict[str, Any]) -> TwitterUser:
"""Create a new Twitter user or update existing one."""
session = self.get_session()
try:
# Check if user already exists
existing_user = session.query(TwitterUser).filter_by(
user_id=user_data['user_id']
).first()
if existing_user:
# Update existing user
for key, value in user_data.items():
if hasattr(existing_user, key) and key != 'id':
setattr(existing_user, key, value)
existing_user.updated_at = datetime.utcnow()
session.commit()
logger.info(f"Updated Twitter user: {existing_user.username}")
return existing_user
else:
# Create new user
twitter_user = create_twitter_user(session, user_data)
logger.info(f"Created new Twitter user: {twitter_user.username}")
return twitter_user
except Exception as e:
session.rollback()
logger.error(f"Error creating/updating user: {e}")
raise
finally:
session.close()
def save_user_credentials(self, user_id: str, credentials: TwitterCredentials) -> bool:
"""Save encrypted Twitter credentials for a user."""
session = self.get_session()
try:
user = session.query(TwitterUser).filter_by(user_id=user_id).first()
if user:
encrypted_creds = self._encrypt_credentials(credentials)
user.credentials_encrypted = encrypted_creds
user.updated_at = datetime.utcnow()
session.commit()
logger.info(f"Saved credentials for user: {user.username}")
return True
else:
logger.error(f"User not found: {user_id}")
return False
except Exception as e:
session.rollback()
logger.error(f"Error saving credentials: {e}")
return False
finally:
session.close()
def get_user_credentials(self, user_id: str) -> Optional[TwitterCredentials]:
"""Get decrypted Twitter credentials for a user."""
session = self.get_session()
try:
user = session.query(TwitterUser).filter_by(user_id=user_id).first()
if user and user.credentials_encrypted:
return self._decrypt_credentials(user.credentials_encrypted)
return None
except Exception as e:
logger.error(f"Error getting credentials: {e}")
return None
finally:
session.close()
def get_user_by_id(self, user_id: str) -> Optional[TwitterUser]:
"""Get Twitter user by ALwrity user ID."""
session = self.get_session()
try:
return session.query(TwitterUser).filter_by(user_id=user_id).first()
finally:
session.close()
def get_user_by_twitter_id(self, twitter_user_id: int) -> Optional[TwitterUser]:
"""Get Twitter user by Twitter user ID."""
session = self.get_session()
try:
return session.query(TwitterUser).filter_by(twitter_user_id=twitter_user_id).first()
finally:
session.close()
def update_user_profile(self, user_id: str, profile_data: Dict[str, Any]) -> bool:
"""Update user profile information from Twitter API."""
session = self.get_session()
try:
user = session.query(TwitterUser).filter_by(user_id=user_id).first()
if user:
update_user_metrics(session, user.id, profile_data)
logger.info(f"Updated profile for user: {user.username}")
return True
return False
except Exception as e:
session.rollback()
logger.error(f"Error updating user profile: {e}")
return False
finally:
session.close()
# --- TWEET MANAGEMENT ---
def save_tweet(self, tweet_data: Dict[str, Any]) -> Tweet:
"""Save a tweet to the database."""
session = self.get_session()
try:
tweet = create_tweet_record(session, tweet_data)
logger.info(f"Saved tweet: {tweet.id}")
return tweet
except Exception as e:
session.rollback()
logger.error(f"Error saving tweet: {e}")
raise
finally:
session.close()
def update_tweet_status(self, tweet_id: int, status: TweetStatus, twitter_tweet_id: Optional[int] = None) -> bool:
"""Update tweet status (e.g., from draft to posted)."""
session = self.get_session()
try:
tweet = session.query(Tweet).filter_by(id=tweet_id).first()
if tweet:
tweet.status = status
if twitter_tweet_id:
tweet.tweet_id = twitter_tweet_id
if status == TweetStatus.POSTED:
tweet.posted_at = datetime.utcnow()
tweet.updated_at = datetime.utcnow()
session.commit()
logger.info(f"Updated tweet {tweet_id} status to {status.value}")
return True
return False
except Exception as e:
session.rollback()
logger.error(f"Error updating tweet status: {e}")
return False
finally:
session.close()
def get_user_tweets(self, user_id: str, status: Optional[TweetStatus] = None, limit: int = 50) -> List[Tweet]:
"""Get tweets for a user, optionally filtered by status."""
session = self.get_session()
try:
user = session.query(TwitterUser).filter_by(user_id=user_id).first()
if not user:
return []
query = session.query(Tweet).filter_by(user_id=user.id)
if status:
query = query.filter_by(status=status)
return query.order_by(desc(Tweet.created_at)).limit(limit).all()
finally:
session.close()
def get_tweet_by_id(self, tweet_id: int) -> Optional[Tweet]:
"""Get tweet by database ID."""
session = self.get_session()
try:
return session.query(Tweet).filter_by(id=tweet_id).first()
finally:
session.close()
def get_tweet_by_twitter_id(self, twitter_tweet_id: int) -> Optional[Tweet]:
"""Get tweet by Twitter tweet ID."""
session = self.get_session()
try:
return session.query(Tweet).filter_by(tweet_id=twitter_tweet_id).first()
finally:
session.close()
def update_tweet_analytics(self, tweet_id: int, metrics: TweetMetrics) -> bool:
"""Update tweet analytics from Twitter API."""
session = self.get_session()
try:
update_tweet_metrics(session, tweet_id, metrics)
logger.info(f"Updated analytics for tweet: {tweet_id}")
return True
except Exception as e:
session.rollback()
logger.error(f"Error updating tweet analytics: {e}")
return False
finally:
session.close()
def get_top_performing_tweets(self, user_id: str, days: int = 30, limit: int = 10) -> List[Tweet]:
"""Get top performing tweets for a user."""
session = self.get_session()
try:
user = session.query(TwitterUser).filter_by(user_id=user_id).first()
if not user:
return []
start_date = datetime.utcnow() - timedelta(days=days)
return session.query(Tweet).filter(
and_(
Tweet.user_id == user.id,
Tweet.status == TweetStatus.POSTED,
Tweet.posted_at >= start_date
)
).order_by(desc(Tweet.engagement_rate)).limit(limit).all()
finally:
session.close()
# --- SCHEDULED TWEETS ---
def schedule_tweet(self, tweet_id: int, scheduled_time: datetime, settings: Dict[str, Any] = None) -> ScheduledTweet:
"""Schedule a tweet for posting."""
session = self.get_session()
try:
tweet = session.query(Tweet).filter_by(id=tweet_id).first()
if not tweet:
raise ValueError(f"Tweet {tweet_id} not found")
scheduled_tweet = ScheduledTweet(
user_id=tweet.user_id,
tweet_id=tweet_id,
scheduled_time=scheduled_time,
timezone=settings.get('timezone', 'UTC'),
auto_optimize_time=settings.get('auto_optimize_time', False),
auto_add_hashtags=settings.get('auto_add_hashtags', False),
auto_add_emojis=settings.get('auto_add_emojis', False)
)
session.add(scheduled_tweet)
# Update tweet status
tweet.status = TweetStatus.SCHEDULED
tweet.scheduled_for = scheduled_time
session.commit()
logger.info(f"Scheduled tweet {tweet_id} for {scheduled_time}")
return scheduled_tweet
except Exception as e:
session.rollback()
logger.error(f"Error scheduling tweet: {e}")
raise
finally:
session.close()
def get_pending_scheduled_tweets(self, user_id: Optional[str] = None) -> List[ScheduledTweet]:
"""Get tweets scheduled for posting."""
session = self.get_session()
try:
query = session.query(ScheduledTweet).filter(
and_(
ScheduledTweet.status == TweetStatus.SCHEDULED,
ScheduledTweet.scheduled_time <= datetime.utcnow()
)
)
if user_id:
user = session.query(TwitterUser).filter_by(user_id=user_id).first()
if user:
query = query.filter_by(user_id=user.id)
return query.order_by(ScheduledTweet.scheduled_time).all()
finally:
session.close()
def mark_scheduled_tweet_posted(self, scheduled_tweet_id: int, twitter_tweet_id: int) -> bool:
"""Mark a scheduled tweet as posted."""
session = self.get_session()
try:
scheduled_tweet = session.query(ScheduledTweet).filter_by(id=scheduled_tweet_id).first()
if scheduled_tweet:
scheduled_tweet.status = TweetStatus.POSTED
# Update the associated tweet
tweet = session.query(Tweet).filter_by(id=scheduled_tweet.tweet_id).first()
if tweet:
tweet.status = TweetStatus.POSTED
tweet.tweet_id = twitter_tweet_id
tweet.posted_at = datetime.utcnow()
session.commit()
logger.info(f"Marked scheduled tweet {scheduled_tweet_id} as posted")
return True
return False
except Exception as e:
session.rollback()
logger.error(f"Error marking scheduled tweet as posted: {e}")
return False
finally:
session.close()
# --- ANALYTICS ---
def save_daily_analytics(self, user_id: str, analytics_data: Dict[str, Any]) -> TwitterAnalytics:
"""Save daily analytics data for a user."""
session = self.get_session()
try:
user = session.query(TwitterUser).filter_by(user_id=user_id).first()
if not user:
raise ValueError(f"User {user_id} not found")
# Check if analytics for today already exist
today = datetime.utcnow().date()
existing = session.query(TwitterAnalytics).filter(
and_(
TwitterAnalytics.user_id == user.id,
func.date(TwitterAnalytics.date) == today,
TwitterAnalytics.timeframe == AnalyticsTimeframe.DAILY
)
).first()
if existing:
# Update existing record
for key, value in analytics_data.items():
if hasattr(existing, key):
setattr(existing, key, value)
existing.updated_at = datetime.utcnow()
session.commit()
return existing
else:
# Create new record
analytics = TwitterAnalytics(
user_id=user.id,
date=datetime.utcnow(),
timeframe=AnalyticsTimeframe.DAILY,
**analytics_data
)
session.add(analytics)
session.commit()
logger.info(f"Saved daily analytics for user: {user.username}")
return analytics
except Exception as e:
session.rollback()
logger.error(f"Error saving analytics: {e}")
raise
finally:
session.close()
def get_analytics_summary(self, user_id: str, days: int = 30) -> Dict[str, Any]:
"""Get comprehensive analytics summary for a user."""
session = self.get_session()
try:
return get_user_analytics_summary(session, user_id, days)
finally:
session.close()
def get_engagement_trends(self, user_id: str, days: int = 30) -> List[Dict[str, Any]]:
"""Get engagement trends over time."""
session = self.get_session()
try:
user = session.query(TwitterUser).filter_by(user_id=user_id).first()
if not user:
return []
start_date = datetime.utcnow() - timedelta(days=days)
analytics = session.query(TwitterAnalytics).filter(
and_(
TwitterAnalytics.user_id == user.id,
TwitterAnalytics.date >= start_date,
TwitterAnalytics.timeframe == AnalyticsTimeframe.DAILY
)
).order_by(TwitterAnalytics.date).all()
return [
{
'date': a.date.isoformat(),
'engagement_rate': a.average_engagement_rate,
'total_engagements': a.total_engagements,
'impressions': a.total_impressions,
'followers_change': a.net_follower_change
}
for a in analytics
]
finally:
session.close()
# --- HASHTAG PERFORMANCE ---
def track_hashtag_performance(self, user_id: str, hashtag: str, tweet_id: int, engagement_metrics: Dict[str, Any]) -> bool:
"""Track performance of a hashtag."""
session = self.get_session()
try:
user = session.query(TwitterUser).filter_by(user_id=user_id).first()
if not user:
return False
# Get or create hashtag performance record
hashtag_perf = session.query(HashtagPerformance).filter(
and_(
HashtagPerformance.user_id == user.id,
HashtagPerformance.hashtag == hashtag
)
).first()
if hashtag_perf:
# Update existing record
hashtag_perf.usage_count += 1
hashtag_perf.total_impressions += engagement_metrics.get('impressions', 0)
hashtag_perf.total_engagements += engagement_metrics.get('engagements', 0)
hashtag_perf.last_used = datetime.utcnow()
# Update average engagement rate
if hashtag_perf.usage_count > 0:
hashtag_perf.average_engagement_rate = (
hashtag_perf.total_engagements / hashtag_perf.total_impressions * 100
if hashtag_perf.total_impressions > 0 else 0
)
# Update best performing tweet if this one is better
current_engagement = engagement_metrics.get('engagements', 0)
if current_engagement > hashtag_perf.best_tweet_engagement:
hashtag_perf.best_tweet_id = tweet_id
hashtag_perf.best_tweet_engagement = current_engagement
else:
# Create new record
hashtag_perf = HashtagPerformance(
user_id=user.id,
hashtag=hashtag,
usage_count=1,
total_impressions=engagement_metrics.get('impressions', 0),
total_engagements=engagement_metrics.get('engagements', 0),
average_engagement_rate=(
engagement_metrics.get('engagements', 0) /
max(engagement_metrics.get('impressions', 1), 1) * 100
),
best_tweet_id=tweet_id,
best_tweet_engagement=engagement_metrics.get('engagements', 0),
first_used=datetime.utcnow(),
last_used=datetime.utcnow()
)
session.add(hashtag_perf)
session.commit()
return True
except Exception as e:
session.rollback()
logger.error(f"Error tracking hashtag performance: {e}")
return False
finally:
session.close()
def get_top_hashtags(self, user_id: str, limit: int = 10) -> List[HashtagPerformance]:
"""Get top performing hashtags for a user."""
session = self.get_session()
try:
user = session.query(TwitterUser).filter_by(user_id=user_id).first()
if not user:
return []
return session.query(HashtagPerformance).filter_by(
user_id=user.id
).order_by(desc(HashtagPerformance.average_engagement_rate)).limit(limit).all()
finally:
session.close()
# --- CONTENT TEMPLATES ---
def save_content_template(self, user_id: str, template_data: Dict[str, Any]) -> ContentTemplate:
"""Save a content template."""
session = self.get_session()
try:
user = session.query(TwitterUser).filter_by(user_id=user_id).first()
if not user:
raise ValueError(f"User {user_id} not found")
template = ContentTemplate(
user_id=user.id,
name=template_data['name'],
description=template_data.get('description', ''),
template_text=template_data['template_text'],
category=ContentCategory(template_data['category']) if template_data.get('category') else None,
variables=template_data.get('variables', []),
default_hashtags=template_data.get('default_hashtags', []),
ai_prompt=template_data.get('ai_prompt', ''),
ai_tone=template_data.get('ai_tone', ''),
ai_target_audience=template_data.get('ai_target_audience', '')
)
session.add(template)
session.commit()
logger.info(f"Saved content template: {template.name}")
return template
except Exception as e:
session.rollback()
logger.error(f"Error saving content template: {e}")
raise
finally:
session.close()
def get_user_templates(self, user_id: str, category: Optional[ContentCategory] = None) -> List[ContentTemplate]:
"""Get content templates for a user."""
session = self.get_session()
try:
user = session.query(TwitterUser).filter_by(user_id=user_id).first()
if not user:
return []
query = session.query(ContentTemplate).filter(
and_(
ContentTemplate.user_id == user.id,
ContentTemplate.is_active == True
)
)
if category:
query = query.filter_by(category=category)
return query.order_by(desc(ContentTemplate.average_performance)).all()
finally:
session.close()
# --- SETTINGS ---
def save_user_settings(self, user_id: str, settings_data: Dict[str, Any]) -> TwitterSettings:
"""Save user Twitter settings."""
session = self.get_session()
try:
user = session.query(TwitterUser).filter_by(user_id=user_id).first()
if not user:
raise ValueError(f"User {user_id} not found")
# Check if settings already exist
existing_settings = session.query(TwitterSettings).filter_by(user_id=user.id).first()
if existing_settings:
# Update existing settings
for key, value in settings_data.items():
if hasattr(existing_settings, key):
setattr(existing_settings, key, value)
existing_settings.updated_at = datetime.utcnow()
session.commit()
return existing_settings
else:
# Create new settings
settings = TwitterSettings(
user_id=user.id,
**settings_data
)
session.add(settings)
session.commit()
logger.info(f"Saved settings for user: {user.username}")
return settings
except Exception as e:
session.rollback()
logger.error(f"Error saving user settings: {e}")
raise
finally:
session.close()
def get_user_settings(self, user_id: str) -> Optional[TwitterSettings]:
"""Get user Twitter settings."""
session = self.get_session()
try:
user = session.query(TwitterUser).filter_by(user_id=user_id).first()
if not user:
return None
return session.query(TwitterSettings).filter_by(user_id=user.id).first()
finally:
session.close()
# --- UTILITY METHODS ---
def cleanup_old_data(self, days_old: int = 30) -> Dict[str, int]:
"""
Clean up old data to maintain database performance.
Args:
days_old: Number of days old data to keep
Returns:
Dictionary with cleanup statistics
"""
try:
cutoff_date = datetime.utcnow() - timedelta(days=days_old)
with self.get_session() as session:
# Clean up old analytics data
old_analytics = session.query(TwitterAnalytics).filter(
TwitterAnalytics.created_at < cutoff_date
).count()
session.query(TwitterAnalytics).filter(
TwitterAnalytics.created_at < cutoff_date
).delete()
# Clean up old tweet analytics
old_tweet_analytics = session.query(TweetAnalytics).filter(
TweetAnalytics.created_at < cutoff_date
).count()
session.query(TweetAnalytics).filter(
TweetAnalytics.created_at < cutoff_date
).delete()
session.commit()
stats = {
'old_analytics_removed': old_analytics,
'old_tweet_analytics_removed': old_tweet_analytics,
'cutoff_date': cutoff_date.isoformat()
}
logger.info(f"Cleaned up old data: {stats}")
return stats
except Exception as e:
logger.error(f"Error cleaning up old data: {e}")
return {'error': str(e)}
def get_database_stats(self) -> Dict[str, int]:
"""
Get database statistics.
Returns:
Dictionary with database statistics
"""
try:
with self.get_session() as session:
stats = {
'total_users': session.query(TwitterUser).count(),
'total_tweets': session.query(Tweet).count(),
'posted_tweets': session.query(Tweet).filter(
Tweet.status == TweetStatus.POSTED
).count(),
'scheduled_tweets': session.query(ScheduledTweet).filter(
ScheduledTweet.status == TweetStatus.SCHEDULED
).count(),
'total_analytics_records': session.query(TwitterAnalytics).count(),
'total_templates': session.query(ContentTemplate).count()
}
return stats
except Exception as e:
logger.error(f"Error getting database stats: {e}")
return {'error': str(e)}
def close(self):
"""
Close database connections and clean up resources.
"""
try:
if hasattr(self, 'engine') and self.engine:
self.engine.dispose()
logger.info("Database connections closed successfully")
except Exception as e:
logger.error(f"Error closing database connections: {e}")
# Create a global instance for easy access
twitter_db = TwitterDatabaseService()
# Export the service and key functions
__all__ = [
'TwitterDatabaseService',
'twitter_db'
]