""" 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' ]