LifeRPG_v2.0/modern/backend/realtime_notifications.py
TLimoges33 2b961611fd
🚀 Major Enhancement: Complete AI-Powered LifeRPG Platform with Git LFS
 New Features:
- AI-powered habit creation with natural language processing
- HuggingFace transformers integration for sentiment analysis (tracked via Git LFS)
- Advanced predictive analytics and behavioral insights
- Voice & image input capabilities for hands-free habit tracking
- Real-time notifications and community features
- Plugin system with extensible architecture

🔧 Technical Improvements:
- Comprehensive FastAPI backend with 30+ endpoints
- React frontend with PWA capabilities
- Advanced authentication with 2FA support
- RBAC authorization system
- Comprehensive security features (CSRF, rate limiting, audit logging)
- Database migrations and health monitoring
- Docker containerization support
- Git LFS configured for large AI model files (2+ GB)

📚 Documentation & DevOps:
- Complete deployment guides for multiple platforms
- Professional README with feature highlights
- GitHub Actions CI/CD workflows
- Comprehensive API documentation
- Security audit roadmap and compliance framework
- Setup scripts for development environment

🧪 Testing & Quality:
- Comprehensive test suite with 20+ test modules
- Setup verification scripts
- Working development environment with both backend and frontend
- Health checks and monitoring systems

🌟 Ready for:
- Portfolio showcasing
- Community contributions
- Production deployment
- Professional presentation
2025-09-28 21:29:19 +00:00

614 lines
22 KiB
Python

"""
Real-time Notifications System with WebSocket Support
Provides instant notifications for habit reminders, achievements, and social interactions
"""
import asyncio
import json
from datetime import datetime, timedelta
from typing import Dict, List, Optional, Any, Set
from dataclasses import dataclass, asdict
from enum import Enum
import uuid
from fastapi import WebSocket, WebSocketDisconnect
from sqlalchemy.orm import Session
from sqlalchemy import text
import logging
from .models import User, Habit, Log
from .db import get_db
logger = logging.getLogger(__name__)
class NotificationType(Enum):
HABIT_REMINDER = "habit_reminder"
ACHIEVEMENT_UNLOCKED = "achievement_unlocked"
QUEST_COMPLETED = "quest_completed"
STREAK_MILESTONE = "streak_milestone"
GUILD_INVITATION = "guild_invitation"
BUDDY_ENCOURAGEMENT = "buddy_encouragement"
CHALLENGE_UPDATE = "challenge_update"
SOCIAL_INTERACTION = "social_interaction"
SYSTEM_ANNOUNCEMENT = "system_announcement"
class NotificationPriority(Enum):
LOW = 1
MEDIUM = 2
HIGH = 3
URGENT = 4
@dataclass
class Notification:
"""Represents a notification to be sent to a user"""
id: str
user_id: int
type: NotificationType
title: str
message: str
data: Dict[str, Any]
priority: NotificationPriority
created_at: datetime
scheduled_for: Optional[datetime] = None
read_at: Optional[datetime] = None
clicked_at: Optional[datetime] = None
action_url: Optional[str] = None
image_url: Optional[str] = None
@dataclass
class HabitReminder:
"""Habit reminder configuration"""
habit_id: int
user_id: int
reminder_time: str # HH:MM format
days_of_week: List[int] # 0=Monday, 6=Sunday
is_active: bool
message_template: str
advance_minutes: int = 0 # Remind X minutes before
class WebSocketManager:
"""Manages WebSocket connections for real-time notifications"""
def __init__(self):
self.active_connections: Dict[int, Set[WebSocket]] = {}
self.connection_metadata: Dict[WebSocket, Dict] = {}
async def connect(self, websocket: WebSocket, user_id: int,
device_info: Optional[Dict] = None):
"""Accept a new WebSocket connection"""
await websocket.accept()
if user_id not in self.active_connections:
self.active_connections[user_id] = set()
self.active_connections[user_id].add(websocket)
self.connection_metadata[websocket] = {
"user_id": user_id,
"connected_at": datetime.now(),
"device_info": device_info or {},
"last_ping": datetime.now()
}
logger.info(f"WebSocket connected for user {user_id}")
# Send connection confirmation
await self.send_to_user(user_id, {
"type": "connection_established",
"timestamp": datetime.now().isoformat(),
"message": "Real-time notifications are now active!"
})
def disconnect(self, websocket: WebSocket):
"""Handle WebSocket disconnection"""
if websocket in self.connection_metadata:
user_id = self.connection_metadata[websocket]["user_id"]
if user_id in self.active_connections:
self.active_connections[user_id].discard(websocket)
# Remove user entry if no more connections
if not self.active_connections[user_id]:
del self.active_connections[user_id]
del self.connection_metadata[websocket]
logger.info(f"WebSocket disconnected for user {user_id}")
async def send_to_user(self, user_id: int, data: Dict):
"""Send data to all connections for a specific user"""
if user_id in self.active_connections:
disconnected_connections = set()
for websocket in self.active_connections[user_id].copy():
try:
await websocket.send_text(json.dumps(data))
except Exception as e:
logger.error(f"Error sending to websocket: {e}")
disconnected_connections.add(websocket)
# Clean up disconnected connections
for websocket in disconnected_connections:
self.disconnect(websocket)
async def send_to_all(self, data: Dict):
"""Send data to all connected users"""
for user_id in list(self.active_connections.keys()):
await self.send_to_user(user_id, data)
async def ping_connections(self):
"""Send ping to maintain connections"""
ping_data = {"type": "ping", "timestamp": datetime.now().isoformat()}
for user_id in list(self.active_connections.keys()):
await self.send_to_user(user_id, ping_data)
def get_connected_users(self) -> List[int]:
"""Get list of currently connected user IDs"""
return list(self.active_connections.keys())
def get_user_connection_count(self, user_id: int) -> int:
"""Get number of active connections for a user"""
return len(self.active_connections.get(user_id, set()))
class NotificationManager:
"""Manages notification creation, scheduling, and delivery"""
def __init__(self, db_session: Session, websocket_manager: WebSocketManager):
self.db = db_session
self.ws_manager = websocket_manager
self.scheduled_notifications: List[Notification] = []
async def create_notification(self, notification: Notification) -> str:
"""Create and optionally schedule a notification"""
# Save to database
query = """
INSERT INTO notifications (id, user_id, type, title, message, data,
priority, created_at, scheduled_for, action_url, image_url)
VALUES (:id, :user_id, :type, :title, :message, :data,
:priority, :created_at, :scheduled_for, :action_url, :image_url)
"""
await self.db.execute(text(query), {
"id": notification.id,
"user_id": notification.user_id,
"type": notification.type.value,
"title": notification.title,
"message": notification.message,
"data": json.dumps(notification.data),
"priority": notification.priority.value,
"created_at": notification.created_at,
"scheduled_for": notification.scheduled_for,
"action_url": notification.action_url,
"image_url": notification.image_url
})
# Send immediately if not scheduled
if notification.scheduled_for is None:
await self._send_notification(notification)
else:
self.scheduled_notifications.append(notification)
return notification.id
async def _send_notification(self, notification: Notification):
"""Send notification via WebSocket and mark as sent"""
notification_data = {
"type": "notification",
"notification": {
"id": notification.id,
"type": notification.type.value,
"title": notification.title,
"message": notification.message,
"priority": notification.priority.value,
"created_at": notification.created_at.isoformat(),
"action_url": notification.action_url,
"image_url": notification.image_url,
"data": notification.data
}
}
await self.ws_manager.send_to_user(notification.user_id, notification_data)
# Log notification sent
logger.info(f"Notification sent: {notification.id} to user {notification.user_id}")
async def send_habit_reminder(self, habit_id: int, user_id: int, custom_message: Optional[str] = None):
"""Send a habit reminder notification"""
# Get habit details
query = """
SELECT title, description FROM habits WHERE id = :habit_id AND user_id = :user_id
"""
result = await self.db.execute(text(query), {"habit_id": habit_id, "user_id": user_id})
habit = result.first()
if not habit:
return
message = custom_message or f"Time to work on your {habit.title} habit!"
notification = Notification(
id=str(uuid.uuid4()),
user_id=user_id,
type=NotificationType.HABIT_REMINDER,
title="🔔 Habit Reminder",
message=message,
data={
"habit_id": habit_id,
"habit_title": habit.title,
"habit_description": habit.description
},
priority=NotificationPriority.MEDIUM,
created_at=datetime.now(),
action_url=f"/habits/{habit_id}"
)
await self.create_notification(notification)
async def send_achievement_notification(self, user_id: int, achievement_data: Dict):
"""Send achievement unlocked notification"""
notification = Notification(
id=str(uuid.uuid4()),
user_id=user_id,
type=NotificationType.ACHIEVEMENT_UNLOCKED,
title="🏆 Achievement Unlocked!",
message=f"Congratulations! You've earned: {achievement_data['title']}",
data=achievement_data,
priority=NotificationPriority.HIGH,
created_at=datetime.now(),
action_url="/achievements",
image_url=achievement_data.get("icon_url")
)
await self.create_notification(notification)
async def send_streak_milestone(self, user_id: int, habit_id: int, streak_count: int):
"""Send streak milestone notification"""
# Get habit title
query = "SELECT title FROM habits WHERE id = :habit_id"
result = await self.db.execute(text(query), {"habit_id": habit_id})
habit = result.first()
if not habit:
return
milestone_messages = {
7: "Amazing! One week strong! 🔥",
14: "Two weeks of consistency! You're on fire! 🌟",
30: "30-day streak! You're a habit hero! 🦸‍♀️",
50: "50 days! Nothing can stop you now! 💪",
100: "100-day milestone! You're absolutely legendary! 👑"
}
message = milestone_messages.get(streak_count, f"{streak_count}-day streak! Keep it up!")
notification = Notification(
id=str(uuid.uuid4()),
user_id=user_id,
type=NotificationType.STREAK_MILESTONE,
title="🔥 Streak Milestone!",
message=f"{habit.title}: {message}",
data={
"habit_id": habit_id,
"habit_title": habit.title,
"streak_count": streak_count
},
priority=NotificationPriority.HIGH,
created_at=datetime.now(),
action_url=f"/habits/{habit_id}"
)
await self.create_notification(notification)
async def send_social_notification(self, user_id: int, notification_data: Dict):
"""Send social interaction notification"""
notification_types = {
"like": "liked your post",
"comment": "commented on your post",
"follow": "started following you",
"buddy_request": "sent you a habit buddy request",
"guild_invite": "invited you to join their guild"
}
action_type = notification_data["action_type"]
from_user = notification_data["from_user"]
message = f"{from_user} {notification_types.get(action_type, 'interacted with you')}"
notification = Notification(
id=str(uuid.uuid4()),
user_id=user_id,
type=NotificationType.SOCIAL_INTERACTION,
title="👥 Social Update",
message=message,
data=notification_data,
priority=NotificationPriority.MEDIUM,
created_at=datetime.now(),
action_url=notification_data.get("action_url", "/social")
)
await self.create_notification(notification)
async def process_scheduled_notifications(self):
"""Process and send scheduled notifications"""
now = datetime.now()
notifications_to_send = []
for notification in self.scheduled_notifications[:]:
if notification.scheduled_for and notification.scheduled_for <= now:
notifications_to_send.append(notification)
self.scheduled_notifications.remove(notification)
for notification in notifications_to_send:
await self._send_notification(notification)
async def get_user_notifications(self, user_id: int, limit: int = 50,
unread_only: bool = False) -> List[Dict]:
"""Get notifications for a user"""
query = """
SELECT * FROM notifications
WHERE user_id = :user_id
"""
if unread_only:
query += " AND read_at IS NULL"
query += " ORDER BY created_at DESC LIMIT :limit"
result = await self.db.execute(text(query), {
"user_id": user_id,
"limit": limit
})
notifications = []
for row in result:
notifications.append({
"id": row.id,
"type": row.type,
"title": row.title,
"message": row.message,
"data": json.loads(row.data or '{}'),
"priority": row.priority,
"created_at": row.created_at,
"read_at": row.read_at,
"action_url": row.action_url,
"image_url": row.image_url
})
return notifications
async def mark_notification_read(self, notification_id: str, user_id: int):
"""Mark a notification as read"""
query = """
UPDATE notifications
SET read_at = :read_at
WHERE id = :notification_id AND user_id = :user_id
"""
await self.db.execute(text(query), {
"notification_id": notification_id,
"user_id": user_id,
"read_at": datetime.now()
})
async def get_unread_count(self, user_id: int) -> int:
"""Get count of unread notifications for a user"""
query = """
SELECT COUNT(*) as unread_count
FROM notifications
WHERE user_id = :user_id AND read_at IS NULL
"""
result = await self.db.execute(text(query), {"user_id": user_id})
row = result.first()
return row.unread_count if row else 0
class HabitReminderService:
"""Manages habit reminders and scheduling"""
def __init__(self, db_session: Session, notification_manager: NotificationManager):
self.db = db_session
self.notification_manager = notification_manager
async def create_habit_reminder(self, reminder_data: Dict) -> str:
"""Create a new habit reminder"""
reminder_id = str(uuid.uuid4())
query = """
INSERT INTO habit_reminders (id, habit_id, user_id, reminder_time,
days_of_week, is_active, message_template, advance_minutes)
VALUES (:id, :habit_id, :user_id, :reminder_time,
:days_of_week, :is_active, :message_template, :advance_minutes)
"""
await self.db.execute(text(query), {
"id": reminder_id,
"habit_id": reminder_data["habit_id"],
"user_id": reminder_data["user_id"],
"reminder_time": reminder_data["reminder_time"],
"days_of_week": json.dumps(reminder_data.get("days_of_week", [0, 1, 2, 3, 4, 5, 6])),
"is_active": reminder_data.get("is_active", True),
"message_template": reminder_data.get("message_template", ""),
"advance_minutes": reminder_data.get("advance_minutes", 0)
})
return reminder_id
async def get_due_reminders(self) -> List[HabitReminder]:
"""Get habit reminders that are due to be sent"""
now = datetime.now()
current_time = now.strftime("%H:%M")
current_weekday = now.weekday()
query = """
SELECT hr.*, h.title as habit_title
FROM habit_reminders hr
JOIN habits h ON hr.habit_id = h.id
WHERE hr.is_active = true
AND hr.reminder_time = :current_time
AND JSON_EXTRACT(hr.days_of_week, '$[*]') LIKE :weekday_pattern
"""
# SQLite JSON query to check if current weekday is in the array
weekday_pattern = f'%{current_weekday}%'
result = await self.db.execute(text(query), {
"current_time": current_time,
"weekday_pattern": weekday_pattern
})
reminders = []
for row in result:
days_of_week = json.loads(row.days_of_week or '[]')
if current_weekday in days_of_week:
reminders.append(HabitReminder(
habit_id=row.habit_id,
user_id=row.user_id,
reminder_time=row.reminder_time,
days_of_week=days_of_week,
is_active=row.is_active,
message_template=row.message_template or f"Time for your {row.habit_title} habit!",
advance_minutes=row.advance_minutes or 0
))
return reminders
async def send_due_reminders(self):
"""Send all due habit reminders"""
reminders = await self.get_due_reminders()
for reminder in reminders:
await self.notification_manager.send_habit_reminder(
habit_id=reminder.habit_id,
user_id=reminder.user_id,
custom_message=reminder.message_template
)
# Global instances
websocket_manager = WebSocketManager()
notification_manager = None # Initialized with database session
async def notification_scheduler():
"""Background task to process scheduled notifications and reminders"""
while True:
try:
if notification_manager:
# Process scheduled notifications
await notification_manager.process_scheduled_notifications()
# Process habit reminders
reminder_service = HabitReminderService(notification_manager.db, notification_manager)
await reminder_service.send_due_reminders()
# Ping WebSocket connections
await websocket_manager.ping_connections()
# Wait 60 seconds before next check
await asyncio.sleep(60)
except Exception as e:
logger.error(f"Error in notification scheduler: {e}")
await asyncio.sleep(60)
# FastAPI WebSocket endpoint
async def websocket_endpoint(websocket: WebSocket, user_id: int):
"""WebSocket endpoint for real-time notifications"""
try:
await websocket_manager.connect(websocket, user_id)
while True:
# Keep connection alive and handle incoming messages
try:
data = await websocket.receive_text()
message = json.loads(data)
# Handle different message types
if message.get("type") == "ping":
await websocket.send_text(json.dumps({
"type": "pong",
"timestamp": datetime.now().isoformat()
}))
elif message.get("type") == "mark_read":
if notification_manager:
await notification_manager.mark_notification_read(
message["notification_id"], user_id
)
except WebSocketDisconnect:
break
except Exception as e:
logger.error(f"WebSocket error: {e}")
break
except Exception as e:
logger.error(f"WebSocket connection error: {e}")
finally:
websocket_manager.disconnect(websocket)
# FastAPI endpoints for notifications
async def get_notifications(user_id: int, unread_only: bool = False,
limit: int = 50, db: Session = None) -> Dict:
"""Get user notifications"""
if not notification_manager:
return {"notifications": [], "unread_count": 0}
notifications = await notification_manager.get_user_notifications(
user_id, limit, unread_only
)
unread_count = await notification_manager.get_unread_count(user_id)
return {
"notifications": notifications,
"unread_count": unread_count,
"has_more": len(notifications) == limit
}
async def mark_notification_read(notification_id: str, user_id: int, db: Session = None):
"""Mark notification as read"""
if notification_manager:
await notification_manager.mark_notification_read(notification_id, user_id)
return {"success": True}
async def create_habit_reminder(user_id: int, reminder_data: Dict, db: Session = None) -> Dict:
"""Create a habit reminder"""
if not notification_manager:
return {"error": "Notification system not available"}
reminder_service = HabitReminderService(db, notification_manager)
reminder_id = await reminder_service.create_habit_reminder({
**reminder_data,
"user_id": user_id
})
return {"reminder_id": reminder_id, "success": True}