Files
site11/services/notifications/backend/preference_manager.py
2025-09-28 20:41:57 +09:00

340 lines
12 KiB
Python

"""
Preference Manager for user notification preferences
"""
import logging
from typing import Optional, Dict, Any, List
from datetime import datetime
import motor.motor_asyncio
from models import NotificationPreference, NotificationChannel, NotificationCategory
logger = logging.getLogger(__name__)
class PreferenceManager:
"""Manages user notification preferences"""
def __init__(self, mongodb_url: str = "mongodb://mongodb:27017", database_name: str = "notifications"):
self.mongodb_url = mongodb_url
self.database_name = database_name
self.client = None
self.db = None
self.preferences_collection = None
self.is_connected = False
# In-memory cache for demo
self.preferences_cache = {}
async def connect(self):
"""Connect to MongoDB"""
try:
self.client = motor.motor_asyncio.AsyncIOMotorClient(self.mongodb_url)
self.db = self.client[self.database_name]
self.preferences_collection = self.db["preferences"]
# Test connection
await self.client.admin.command('ping')
self.is_connected = True
# Create indexes
await self._create_indexes()
logger.info("Connected to MongoDB for preferences")
except Exception as e:
logger.error(f"Failed to connect to MongoDB: {e}")
# Fallback to in-memory storage
self.is_connected = False
logger.warning("Using in-memory storage for preferences")
async def close(self):
"""Close MongoDB connection"""
if self.client:
self.client.close()
self.is_connected = False
logger.info("Disconnected from MongoDB")
async def _create_indexes(self):
"""Create database indexes"""
if self.preferences_collection:
try:
await self.preferences_collection.create_index("user_id", unique=True)
logger.info("Created indexes for preferences collection")
except Exception as e:
logger.error(f"Failed to create indexes: {e}")
async def get_user_preferences(self, user_id: str) -> Optional[NotificationPreference]:
"""Get notification preferences for a user"""
try:
# Check cache first
if user_id in self.preferences_cache:
return self.preferences_cache[user_id]
if self.is_connected and self.preferences_collection:
# Get from MongoDB
doc = await self.preferences_collection.find_one({"user_id": user_id})
if doc:
# Convert document to model
doc.pop('_id', None) # Remove MongoDB ID
preference = NotificationPreference(**doc)
# Update cache
self.preferences_cache[user_id] = preference
return preference
# Return default preferences if not found
return self._get_default_preferences(user_id)
except Exception as e:
logger.error(f"Failed to get preferences for user {user_id}: {e}")
return self._get_default_preferences(user_id)
async def update_user_preferences(
self,
user_id: str,
preferences: NotificationPreference
) -> bool:
"""Update notification preferences for a user"""
try:
preferences.user_id = user_id
preferences.updated_at = datetime.now()
# Update cache
self.preferences_cache[user_id] = preferences
if self.is_connected and self.preferences_collection:
# Convert to dict for MongoDB
pref_dict = preferences.dict()
# Upsert in MongoDB
result = await self.preferences_collection.update_one(
{"user_id": user_id},
{"$set": pref_dict},
upsert=True
)
logger.info(f"Updated preferences for user {user_id}")
return result.modified_count > 0 or result.upserted_id is not None
# If not connected, just use cache
return True
except Exception as e:
logger.error(f"Failed to update preferences for user {user_id}: {e}")
return False
async def unsubscribe_category(self, user_id: str, category: str) -> bool:
"""Unsubscribe user from a notification category"""
try:
preferences = await self.get_user_preferences(user_id)
if not preferences:
preferences = self._get_default_preferences(user_id)
# Update category preference
if hasattr(NotificationCategory, category.upper()):
cat_enum = NotificationCategory(category.lower())
preferences.categories[cat_enum] = False
# Save updated preferences
return await self.update_user_preferences(user_id, preferences)
return False
except Exception as e:
logger.error(f"Failed to unsubscribe user {user_id} from {category}: {e}")
return False
async def subscribe_category(self, user_id: str, category: str) -> bool:
"""Subscribe user to a notification category"""
try:
preferences = await self.get_user_preferences(user_id)
if not preferences:
preferences = self._get_default_preferences(user_id)
# Update category preference
if hasattr(NotificationCategory, category.upper()):
cat_enum = NotificationCategory(category.lower())
preferences.categories[cat_enum] = True
# Save updated preferences
return await self.update_user_preferences(user_id, preferences)
return False
except Exception as e:
logger.error(f"Failed to subscribe user {user_id} to {category}: {e}")
return False
async def enable_channel(self, user_id: str, channel: NotificationChannel) -> bool:
"""Enable a notification channel for user"""
try:
preferences = await self.get_user_preferences(user_id)
if not preferences:
preferences = self._get_default_preferences(user_id)
preferences.channels[channel] = True
return await self.update_user_preferences(user_id, preferences)
except Exception as e:
logger.error(f"Failed to enable channel {channel} for user {user_id}: {e}")
return False
async def disable_channel(self, user_id: str, channel: NotificationChannel) -> bool:
"""Disable a notification channel for user"""
try:
preferences = await self.get_user_preferences(user_id)
if not preferences:
preferences = self._get_default_preferences(user_id)
preferences.channels[channel] = False
return await self.update_user_preferences(user_id, preferences)
except Exception as e:
logger.error(f"Failed to disable channel {channel} for user {user_id}: {e}")
return False
async def set_quiet_hours(
self,
user_id: str,
start_time: str,
end_time: str
) -> bool:
"""Set quiet hours for user"""
try:
preferences = await self.get_user_preferences(user_id)
if not preferences:
preferences = self._get_default_preferences(user_id)
preferences.quiet_hours = {
"start": start_time,
"end": end_time
}
return await self.update_user_preferences(user_id, preferences)
except Exception as e:
logger.error(f"Failed to set quiet hours for user {user_id}: {e}")
return False
async def clear_quiet_hours(self, user_id: str) -> bool:
"""Clear quiet hours for user"""
try:
preferences = await self.get_user_preferences(user_id)
if not preferences:
preferences = self._get_default_preferences(user_id)
preferences.quiet_hours = None
return await self.update_user_preferences(user_id, preferences)
except Exception as e:
logger.error(f"Failed to clear quiet hours for user {user_id}: {e}")
return False
async def set_email_frequency(self, user_id: str, frequency: str) -> bool:
"""Set email notification frequency"""
try:
if frequency not in ["immediate", "daily", "weekly"]:
return False
preferences = await self.get_user_preferences(user_id)
if not preferences:
preferences = self._get_default_preferences(user_id)
preferences.email_frequency = frequency
return await self.update_user_preferences(user_id, preferences)
except Exception as e:
logger.error(f"Failed to set email frequency for user {user_id}: {e}")
return False
async def batch_get_preferences(self, user_ids: List[str]) -> Dict[str, NotificationPreference]:
"""Get preferences for multiple users"""
results = {}
for user_id in user_ids:
pref = await self.get_user_preferences(user_id)
if pref:
results[user_id] = pref
return results
async def delete_user_preferences(self, user_id: str) -> bool:
"""Delete all preferences for a user"""
try:
# Remove from cache
if user_id in self.preferences_cache:
del self.preferences_cache[user_id]
if self.is_connected and self.preferences_collection:
# Delete from MongoDB
result = await self.preferences_collection.delete_one({"user_id": user_id})
logger.info(f"Deleted preferences for user {user_id}")
return result.deleted_count > 0
return True
except Exception as e:
logger.error(f"Failed to delete preferences for user {user_id}: {e}")
return False
def _get_default_preferences(self, user_id: str) -> NotificationPreference:
"""Get default notification preferences"""
return NotificationPreference(
user_id=user_id,
channels={
NotificationChannel.EMAIL: True,
NotificationChannel.SMS: False,
NotificationChannel.PUSH: True,
NotificationChannel.IN_APP: True
},
categories={
NotificationCategory.SYSTEM: True,
NotificationCategory.MARKETING: False,
NotificationCategory.TRANSACTION: True,
NotificationCategory.SOCIAL: True,
NotificationCategory.SECURITY: True,
NotificationCategory.UPDATE: True
},
email_frequency="immediate",
timezone="UTC",
language="en"
)
async def is_notification_allowed(
self,
user_id: str,
channel: NotificationChannel,
category: NotificationCategory
) -> bool:
"""Check if notification is allowed based on preferences"""
preferences = await self.get_user_preferences(user_id)
if not preferences:
return True # Allow by default if no preferences
# Check channel preference
if not preferences.channels.get(channel, True):
return False
# Check category preference
if not preferences.categories.get(category, True):
return False
# Check quiet hours
if preferences.quiet_hours and channel != NotificationChannel.IN_APP:
# Would need to check current time against quiet hours
# For demo, we'll allow all
pass
return True