Added new features to the project

This commit is contained in:
ajaysi
2025-06-30 07:49:48 +05:30
parent bbe56a364d
commit b21cbb68da
48 changed files with 19774 additions and 1889 deletions

View File

@@ -1,26 +1,32 @@
"""
Twitter platform adapter implementation.
Twitter platform adapter implementation with enhanced error handling and real metrics.
"""
from typing import Dict, Any, Optional, List
from datetime import datetime
import tweepy
from tweepy.models import Status
import logging
import time
from .base import PlatformAdapter
logger = logging.getLogger(__name__)
class TwitterAdapter(PlatformAdapter):
"""Twitter platform adapter."""
"""Enhanced Twitter platform adapter with real metrics and error handling."""
def __init__(self, config: Dict[str, Any]):
"""Initialize Twitter adapter with configuration."""
super().__init__(config)
self._validate_config()
self._initialize_client()
self.rate_limit_tracker = {}
def _initialize_client(self) -> None:
"""Initialize Twitter API client."""
"""Initialize Twitter API client with enhanced error handling."""
try:
# Initialize OAuth handler
auth = tweepy.OAuthHandler(
self.config['api_key'],
self.config['api_secret']
@@ -29,31 +35,54 @@ class TwitterAdapter(PlatformAdapter):
self.config['access_token'],
self.config['access_token_secret']
)
self.client = tweepy.API(auth)
self.client.verify_credentials()
except Exception as e:
raise Exception(
f"Failed to initialize Twitter client: {str(e)}"
# Create API client with wait_on_rate_limit
self.client = tweepy.API(
auth,
wait_on_rate_limit=True,
retry_count=3,
retry_delay=5
)
# Verify credentials
user = self.client.verify_credentials()
if not user:
raise Exception("Failed to verify Twitter credentials")
logger.info(f"Twitter client initialized for @{user.screen_name}")
except tweepy.Unauthorized:
raise Exception("Invalid Twitter API credentials")
except tweepy.Forbidden:
raise Exception("Access forbidden - check API permissions")
except Exception as e:
raise Exception(f"Failed to initialize Twitter client: {str(e)}")
async def publish_content(
self,
content: Dict[str, Any],
schedule_time: Optional[datetime] = None
) -> Dict[str, Any]:
"""Publish content to Twitter."""
"""Publish content to Twitter with enhanced error handling."""
try:
# Validate content
# Validate content first
validation = await self.validate_content(content)
if not validation.get('success'):
return validation
# Check rate limits
if not self._check_rate_limit('tweets'):
return self._format_error_response(
Exception("Rate limit exceeded for tweets"),
{'content': content}
)
# Prepare tweet content
tweet_text = content.get('text', '')
media_ids = []
# Handle media attachments if present
if 'media' in content:
if 'media' in content and content['media']:
for media in content['media']:
media_id = self._upload_media(media)
if media_id:
@@ -65,37 +94,348 @@ class TwitterAdapter(PlatformAdapter):
media_ids=media_ids if media_ids else None
)
return self._format_success_response({
'id': tweet.id_str,
'text': tweet.text,
'created_at': tweet.created_at.isoformat()
})
# Update rate limit tracker
self._update_rate_limit_tracker('tweets')
except Exception as e:
return self._format_error_response(
e,
{'content': content, 'schedule_time': schedule_time}
)
async def get_content_status(
self,
content_id: str
) -> Dict[str, Any]:
"""Get status of a tweet."""
try:
tweet = self.client.get_status(content_id)
return self._format_success_response({
# Format response with comprehensive data
tweet_data = {
'id': tweet.id_str,
'text': tweet.text,
'created_at': tweet.created_at.isoformat(),
'favorite_count': tweet.favorite_count,
'retweet_count': tweet.retweet_count
})
except Exception as e:
'user': {
'screen_name': tweet.user.screen_name,
'name': tweet.user.name,
'followers_count': tweet.user.followers_count
},
'metrics': {
'retweet_count': tweet.retweet_count,
'favorite_count': tweet.favorite_count,
'reply_count': getattr(tweet, 'reply_count', 0)
},
'urls': {
'tweet_url': f"https://twitter.com/{tweet.user.screen_name}/status/{tweet.id_str}"
}
}
return self._format_success_response(tweet_data)
except tweepy.Unauthorized:
return self._format_error_response(
e,
Exception("Authentication failed - please reconnect your account"),
{'content': content}
)
except tweepy.Forbidden as e:
error_msg = "Access forbidden"
if "duplicate" in str(e).lower():
error_msg = "Duplicate tweet detected - please modify your content"
elif "automated" in str(e).lower():
error_msg = "Tweet appears automated - please make it more personal"
return self._format_error_response(
Exception(error_msg),
{'content': content}
)
except tweepy.TooManyRequests:
return self._format_error_response(
Exception("Rate limit exceeded - please wait before posting again"),
{'content': content}
)
except Exception as e:
return self._format_error_response(e, {'content': content})
async def get_content_status(self, content_id: str) -> Dict[str, Any]:
"""Get status of a tweet with real metrics."""
try:
tweet = self.client.get_status(
content_id,
include_entities=True,
tweet_mode='extended'
)
tweet_data = {
'id': tweet.id_str,
'text': tweet.full_text,
'created_at': tweet.created_at.isoformat(),
'metrics': {
'retweet_count': tweet.retweet_count,
'favorite_count': tweet.favorite_count,
'reply_count': getattr(tweet, 'reply_count', 0),
'quote_count': getattr(tweet, 'quote_count', 0)
},
'engagement': {
'engagement_rate': self._calculate_engagement_rate(tweet),
'total_engagement': tweet.retweet_count + tweet.favorite_count + getattr(tweet, 'reply_count', 0)
},
'user': {
'screen_name': tweet.user.screen_name,
'followers_count': tweet.user.followers_count
}
}
return self._format_success_response(tweet_data)
except tweepy.NotFound:
return self._format_error_response(
Exception("Tweet not found - it may have been deleted"),
{'content_id': content_id}
)
except Exception as e:
return self._format_error_response(e, {'content_id': content_id})
async def get_analytics(
self,
content_id: str,
start_date: Optional[datetime] = None,
end_date: Optional[datetime] = None
) -> Dict[str, Any]:
"""Get comprehensive analytics for a tweet."""
try:
# Get tweet details
tweet = self.client.get_status(
content_id,
include_entities=True,
tweet_mode='extended'
)
# Calculate engagement metrics
total_engagement = (
tweet.retweet_count +
tweet.favorite_count +
getattr(tweet, 'reply_count', 0) +
getattr(tweet, 'quote_count', 0)
)
engagement_rate = self._calculate_engagement_rate(tweet)
# Get time-based metrics (if tweet is recent)
time_metrics = self._calculate_time_metrics(tweet)
analytics_data = {
'tweet_id': tweet.id_str,
'metrics': {
'likes': tweet.favorite_count,
'retweets': tweet.retweet_count,
'replies': getattr(tweet, 'reply_count', 0),
'quotes': getattr(tweet, 'quote_count', 0),
'total_engagement': total_engagement,
'impressions': getattr(tweet, 'impression_count', 0) # May not be available
},
'engagement': {
'engagement_rate': engagement_rate,
'likes_rate': (tweet.favorite_count / tweet.user.followers_count * 100) if tweet.user.followers_count > 0 else 0,
'retweets_rate': (tweet.retweet_count / tweet.user.followers_count * 100) if tweet.user.followers_count > 0 else 0
},
'timing': time_metrics,
'audience': {
'followers_at_post': tweet.user.followers_count,
'reach_percentage': (total_engagement / tweet.user.followers_count * 100) if tweet.user.followers_count > 0 else 0
},
'content_analysis': {
'character_count': len(tweet.full_text),
'hashtag_count': len([entity for entity in tweet.entities.get('hashtags', [])]),
'mention_count': len([entity for entity in tweet.entities.get('user_mentions', [])]),
'url_count': len([entity for entity in tweet.entities.get('urls', [])])
}
}
return self._format_success_response(analytics_data)
except Exception as e:
return self._format_error_response(e, {
'content_id': content_id,
'start_date': start_date,
'end_date': end_date
})
async def validate_content(self, content: Dict[str, Any]) -> Dict[str, Any]:
"""Enhanced content validation."""
try:
errors = []
warnings = []
# Check text
text = content.get('text', '')
if not text.strip():
errors.append("Tweet text cannot be empty")
# Check length
if len(text) > 280:
errors.append(f"Tweet text exceeds 280 characters ({len(text)}/280)")
elif len(text) > 270:
warnings.append("Tweet is close to character limit")
# Check for very short tweets
if len(text) < 10:
warnings.append("Very short tweets may get less engagement")
# Check media
media = content.get('media', [])
if len(media) > 4:
errors.append("Maximum 4 media attachments allowed")
# Check for spam indicators
if text.count('#') > 3:
warnings.append("Too many hashtags may reduce engagement")
if text.count('@') > 5:
warnings.append("Too many mentions may appear spammy")
# Check for duplicate content (basic check)
if self._is_potential_duplicate(text):
warnings.append("Content may be similar to recent tweets")
if errors:
return self._format_error_response(
ValueError(f"Validation failed: {'; '.join(errors)}"),
{'content': content, 'warnings': warnings}
)
validation_data = {
'valid': True,
'content': content,
'warnings': warnings,
'suggestions': self._get_content_suggestions(text)
}
return self._format_success_response(validation_data)
except Exception as e:
return self._format_error_response(e, {'content': content})
def _calculate_engagement_rate(self, tweet: Status) -> float:
"""Calculate engagement rate for a tweet."""
try:
total_engagement = (
tweet.favorite_count +
tweet.retweet_count +
getattr(tweet, 'reply_count', 0) +
getattr(tweet, 'quote_count', 0)
)
followers = tweet.user.followers_count
return (total_engagement / followers * 100) if followers > 0 else 0.0
except Exception:
return 0.0
def _calculate_time_metrics(self, tweet: Status) -> Dict[str, Any]:
"""Calculate time-based metrics for a tweet."""
try:
now = datetime.now()
tweet_time = tweet.created_at.replace(tzinfo=None)
age_hours = (now - tweet_time).total_seconds() / 3600
# Calculate engagement velocity (engagement per hour)
total_engagement = (
tweet.favorite_count +
tweet.retweet_count +
getattr(tweet, 'reply_count', 0)
)
engagement_velocity = total_engagement / max(age_hours, 1)
return {
'age_hours': round(age_hours, 2),
'engagement_velocity': round(engagement_velocity, 2),
'peak_engagement_period': self._estimate_peak_period(tweet_time),
'posted_at': tweet_time.isoformat()
}
except Exception:
return {}
def _estimate_peak_period(self, tweet_time: datetime) -> str:
"""Estimate if tweet was posted during peak engagement period."""
hour = tweet_time.hour
if 9 <= hour <= 10:
return "Morning Peak (9-10 AM)"
elif 12 <= hour <= 13:
return "Lunch Peak (12-1 PM)"
elif 19 <= hour <= 21:
return "Evening Peak (7-9 PM)"
else:
return "Off-Peak Hours"
def _check_rate_limit(self, endpoint: str) -> bool:
"""Check if we're within rate limits for an endpoint."""
try:
rate_limits = self.client.get_rate_limit_status()
endpoint_map = {
'tweets': '/statuses/update',
'user_timeline': '/statuses/user_timeline',
'verify_credentials': '/account/verify_credentials'
}
if endpoint in endpoint_map:
limit_info = rate_limits['resources']['statuses'].get(endpoint_map[endpoint])
if limit_info:
return limit_info['remaining'] > 0
return True # Default to allowing if we can't check
except Exception:
return True # Default to allowing if check fails
def _update_rate_limit_tracker(self, endpoint: str) -> None:
"""Update internal rate limit tracker."""
now = time.time()
if endpoint not in self.rate_limit_tracker:
self.rate_limit_tracker[endpoint] = []
# Add current request
self.rate_limit_tracker[endpoint].append(now)
# Clean old requests (older than 15 minutes)
self.rate_limit_tracker[endpoint] = [
timestamp for timestamp in self.rate_limit_tracker[endpoint]
if now - timestamp < 900 # 15 minutes
]
def _is_potential_duplicate(self, text: str) -> bool:
"""Basic check for potential duplicate content."""
# This is a simplified check - in production, you'd want more sophisticated detection
try:
# Get recent tweets from user
recent_tweets = self.client.user_timeline(count=20, tweet_mode='extended')
for tweet in recent_tweets:
# Simple similarity check
if self._calculate_text_similarity(text, tweet.full_text) > 0.8:
return True
return False
except Exception:
return False # If we can't check, assume it's not a duplicate
def _calculate_text_similarity(self, text1: str, text2: str) -> float:
"""Calculate simple text similarity."""
# Simple word-based similarity
words1 = set(text1.lower().split())
words2 = set(text2.lower().split())
if not words1 or not words2:
return 0.0
intersection = words1.intersection(words2)
union = words1.union(words2)
return len(intersection) / len(union) if union else 0.0
def _get_content_suggestions(self, text: str) -> List[str]:
"""Get suggestions for improving tweet content."""
suggestions = []
if len(text) < 50:
suggestions.append("Consider adding more context to increase engagement")
if not any(char in text for char in '!?'):
suggestions.append("Adding punctuation can make tweets more engaging")
if '#' not in text:
suggestions.append("Consider adding 1-2 relevant hashtags")
if not any(emoji_char in text for emoji_char in '😀😃😄😁😆😅😂🤣'):
suggestions.append("Emojis can increase engagement and visual appeal")
return suggestions
async def delete_content(
self,
@@ -134,68 +474,6 @@ class TwitterAdapter(PlatformAdapter):
}
)
async def get_analytics(
self,
content_id: str,
start_date: Optional[datetime] = None,
end_date: Optional[datetime] = None
) -> Dict[str, Any]:
"""Get analytics for a tweet."""
try:
tweet = self.client.get_status(content_id)
return self._format_success_response({
'id': tweet.id_str,
'metrics': {
'favorites': tweet.favorite_count,
'retweets': tweet.retweet_count,
'replies': tweet.reply_count if hasattr(tweet, 'reply_count') else 0,
'impressions': tweet.impression_count if hasattr(tweet, 'impression_count') else 0
},
'engagement_rate': self._calculate_engagement_rate(tweet)
})
except Exception as e:
return self._format_error_response(
e,
{
'content_id': content_id,
'start_date': start_date,
'end_date': end_date
}
)
async def validate_content(
self,
content: Dict[str, Any]
) -> Dict[str, Any]:
"""Validate content before publishing."""
try:
# Check text length
text = content.get('text', '')
if len(text) > 280:
return self._format_error_response(
ValueError("Tweet text exceeds 280 characters"),
{'content': content}
)
# Check media attachments
media = content.get('media', [])
if len(media) > 4:
return self._format_error_response(
ValueError("Maximum 4 media attachments allowed"),
{'content': content}
)
return self._format_success_response({
'valid': True,
'content': content
})
except Exception as e:
return self._format_error_response(
e,
{'content': content}
)
async def get_optimal_publish_time(
self,
content_type: str,
@@ -245,19 +523,6 @@ class TwitterAdapter(PlatformAdapter):
except Exception as e:
return self._format_error_response(e)
def _calculate_engagement_rate(self, tweet: Status) -> float:
"""Calculate engagement rate for a tweet."""
try:
total_engagement = (
tweet.favorite_count +
tweet.retweet_count +
(tweet.reply_count if hasattr(tweet, 'reply_count') else 0)
)
followers = tweet.user.followers_count
return (total_engagement / followers * 100) if followers > 0 else 0.0
except Exception:
return 0.0
def _upload_media(self, media: Dict[str, Any]) -> Optional[str]:
"""Upload media to Twitter."""
try:

View File

@@ -0,0 +1,337 @@
"""
Twitter Authentication Bridge
Connects the platform adapter with the UI authentication system for secure Twitter integration.
"""
import streamlit as st
import tweepy
import json
import os
from typing import Dict, Any, Optional, Tuple
from datetime import datetime, timedelta
from pathlib import Path
import hashlib
import base64
from cryptography.fernet import Fernet
import logging
from .platform_adapters.twitter import TwitterAdapter
logger = logging.getLogger(__name__)
class TwitterAuthBridge:
"""Bridge between Twitter authentication and platform adapter."""
def __init__(self):
self.config_dir = Path("config/twitter")
self.config_dir.mkdir(parents=True, exist_ok=True)
self.encryption_key = self._get_or_create_encryption_key()
def _get_or_create_encryption_key(self) -> bytes:
"""Get or create encryption key for secure credential storage."""
key_file = self.config_dir / "encryption.key"
if key_file.exists():
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: Dict[str, str]) -> str:
"""Encrypt Twitter credentials for secure storage."""
try:
fernet = Fernet(self.encryption_key)
credentials_json = json.dumps(credentials)
encrypted_data = fernet.encrypt(credentials_json.encode())
return base64.b64encode(encrypted_data).decode()
except Exception as e:
logger.error(f"Failed to encrypt credentials: {str(e)}")
raise
def decrypt_credentials(self, encrypted_data: str) -> Dict[str, str]:
"""Decrypt Twitter credentials from secure storage."""
try:
fernet = Fernet(self.encryption_key)
encrypted_bytes = base64.b64decode(encrypted_data.encode())
decrypted_data = fernet.decrypt(encrypted_bytes)
return json.loads(decrypted_data.decode())
except Exception as e:
logger.error(f"Failed to decrypt credentials: {str(e)}")
raise
def save_credentials(self, user_id: str, credentials: Dict[str, str]) -> bool:
"""Save encrypted Twitter credentials to file."""
try:
# Create user-specific credentials file
user_hash = hashlib.sha256(user_id.encode()).hexdigest()[:16]
creds_file = self.config_dir / f"user_{user_hash}.enc"
# Add timestamp and validation
credentials_with_meta = {
**credentials,
'created_at': datetime.now().isoformat(),
'user_id_hash': user_hash
}
# Encrypt and save
encrypted_data = self.encrypt_credentials(credentials_with_meta)
with open(creds_file, 'w') as f:
f.write(encrypted_data)
logger.info(f"Credentials saved for user {user_hash}")
return True
except Exception as e:
logger.error(f"Failed to save credentials: {str(e)}")
return False
def load_credentials(self, user_id: str) -> Optional[Dict[str, str]]:
"""Load and decrypt Twitter credentials from file."""
try:
user_hash = hashlib.sha256(user_id.encode()).hexdigest()[:16]
creds_file = self.config_dir / f"user_{user_hash}.enc"
if not creds_file.exists():
logger.warning(f"No credentials found for user {user_hash}")
return None
# Load and decrypt
with open(creds_file, 'r') as f:
encrypted_data = f.read()
credentials = self.decrypt_credentials(encrypted_data)
# Validate credentials are not expired (optional)
created_at = datetime.fromisoformat(credentials.get('created_at', ''))
if datetime.now() - created_at > timedelta(days=365): # 1 year expiry
logger.warning(f"Credentials expired for user {user_hash}")
return None
# Remove metadata before returning
clean_credentials = {k: v for k, v in credentials.items()
if k not in ['created_at', 'user_id_hash']}
return clean_credentials
except Exception as e:
logger.error(f"Failed to load credentials: {str(e)}")
return None
def delete_credentials(self, user_id: str) -> bool:
"""Delete stored Twitter credentials."""
try:
user_hash = hashlib.sha256(user_id.encode()).hexdigest()[:16]
creds_file = self.config_dir / f"user_{user_hash}.enc"
if creds_file.exists():
creds_file.unlink()
logger.info(f"Credentials deleted for user {user_hash}")
return True
except Exception as e:
logger.error(f"Failed to delete credentials: {str(e)}")
return False
def validate_credentials(self, credentials: Dict[str, str]) -> Tuple[bool, str]:
"""Validate Twitter API credentials."""
try:
# Check required fields
required_fields = ['api_key', 'api_secret', 'access_token', 'access_token_secret']
missing_fields = [field for field in required_fields if not credentials.get(field)]
if missing_fields:
return False, f"Missing required fields: {', '.join(missing_fields)}"
# Test connection
auth = tweepy.OAuthHandler(
credentials['api_key'],
credentials['api_secret']
)
auth.set_access_token(
credentials['access_token'],
credentials['access_token_secret']
)
api = tweepy.API(auth)
user = api.verify_credentials()
if user:
return True, f"Valid credentials for @{user.screen_name}"
else:
return False, "Failed to verify credentials"
except tweepy.Unauthorized:
return False, "Invalid API credentials"
except tweepy.Forbidden:
return False, "Access forbidden - check API permissions"
except tweepy.TooManyRequests:
return False, "Rate limit exceeded - try again later"
except Exception as e:
return False, f"Connection error: {str(e)}"
def get_twitter_adapter(self, user_id: str) -> Optional[TwitterAdapter]:
"""Get configured Twitter adapter for user."""
try:
# First check session state
if 'twitter_adapter' in st.session_state:
return st.session_state.twitter_adapter
# Load credentials
credentials = self.load_credentials(user_id)
if not credentials:
return None
# Validate credentials
is_valid, message = self.validate_credentials(credentials)
if not is_valid:
logger.error(f"Invalid credentials: {message}")
return None
# Create adapter
adapter = TwitterAdapter(credentials)
# Cache in session state
st.session_state.twitter_adapter = adapter
return adapter
except Exception as e:
logger.error(f"Failed to get Twitter adapter: {str(e)}")
return None
def get_user_info(self, user_id: str) -> Optional[Dict[str, Any]]:
"""Get Twitter user information."""
try:
adapter = self.get_twitter_adapter(user_id)
if not adapter:
return None
# Get user info from Twitter
user = adapter.client.verify_credentials()
user_info = {
'id': user.id_str,
'screen_name': user.screen_name,
'name': user.name,
'description': user.description,
'followers_count': user.followers_count,
'friends_count': user.friends_count,
'statuses_count': user.statuses_count,
'profile_image_url': user.profile_image_url_https,
'profile_banner_url': getattr(user, 'profile_banner_url', ''),
'verified': user.verified,
'created_at': user.created_at.isoformat(),
'location': user.location or '',
'url': user.url or ''
}
return user_info
except Exception as e:
logger.error(f"Failed to get user info: {str(e)}")
return None
def setup_session_state(self, user_id: str) -> bool:
"""Setup session state with Twitter authentication."""
try:
# Load credentials
credentials = self.load_credentials(user_id)
if not credentials:
return False
# Get user info
user_info = self.get_user_info(user_id)
if not user_info:
return False
# Setup session state
st.session_state.twitter_authenticated = True
st.session_state.twitter_user_id = user_id
st.session_state.twitter_user_info = user_info
st.session_state.twitter_config = credentials
return True
except Exception as e:
logger.error(f"Failed to setup session state: {str(e)}")
return False
def clear_session_state(self) -> None:
"""Clear Twitter authentication from session state."""
keys_to_clear = [
'twitter_authenticated',
'twitter_user_id',
'twitter_user_info',
'twitter_config',
'twitter_adapter'
]
for key in keys_to_clear:
if key in st.session_state:
del st.session_state[key]
def is_authenticated(self) -> bool:
"""Check if user is authenticated with Twitter."""
return (
st.session_state.get('twitter_authenticated', False) and
st.session_state.get('twitter_user_info') is not None and
st.session_state.get('twitter_config') is not None
)
def get_rate_limit_status(self, user_id: str) -> Optional[Dict[str, Any]]:
"""Get current rate limit status."""
try:
adapter = self.get_twitter_adapter(user_id)
if not adapter:
return None
rate_limits = adapter.client.get_rate_limit_status()
# Extract relevant rate limits
relevant_limits = {
'tweets': rate_limits['resources']['statuses']['/statuses/update'],
'user_timeline': rate_limits['resources']['statuses']['/statuses/user_timeline'],
'verify_credentials': rate_limits['resources']['account']['/account/verify_credentials']
}
return relevant_limits
except Exception as e:
logger.error(f"Failed to get rate limit status: {str(e)}")
return None
# Global instance
twitter_auth = TwitterAuthBridge()
# Convenience functions for UI
def save_twitter_credentials(user_id: str, credentials: Dict[str, str]) -> bool:
"""Save Twitter credentials (convenience function)."""
return twitter_auth.save_credentials(user_id, credentials)
def load_twitter_credentials(user_id: str) -> Optional[Dict[str, str]]:
"""Load Twitter credentials (convenience function)."""
return twitter_auth.load_credentials(user_id)
def get_twitter_adapter(user_id: str) -> Optional[TwitterAdapter]:
"""Get Twitter adapter (convenience function)."""
return twitter_auth.get_twitter_adapter(user_id)
def is_twitter_authenticated() -> bool:
"""Check if Twitter is authenticated (convenience function)."""
return twitter_auth.is_authenticated()
def setup_twitter_session(user_id: str) -> bool:
"""Setup Twitter session (convenience function)."""
return twitter_auth.setup_session_state(user_id)
def clear_twitter_session() -> None:
"""Clear Twitter session (convenience function)."""
twitter_auth.clear_session_state()
def validate_twitter_credentials(credentials: Dict[str, str]) -> Tuple[bool, str]:
"""Validate Twitter credentials (convenience function)."""
return twitter_auth.validate_credentials(credentials)