Added new features to the project
This commit is contained in:
337
lib/integrations/twitter_auth_bridge.py
Normal file
337
lib/integrations/twitter_auth_bridge.py
Normal 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)
|
||||
Reference in New Issue
Block a user