
Complete rewrite from task management to full-featured chat system: Core Features: - Real-time WebSocket messaging with connection management - Direct messages and group chats with admin controls - Message types: text, images, videos, audio, documents - Message status tracking: sent, delivered, read receipts - Typing indicators and user presence (online/offline) - Message replies, editing, and deletion Security & Encryption: - End-to-end encryption with RSA + AES hybrid approach - JWT authentication for API and WebSocket connections - Secure file storage with access control - Automatic RSA key pair generation per user Media & File Sharing: - Multi-format file upload (images, videos, audio, documents) - Automatic thumbnail generation for images/videos - File size validation and MIME type checking - Secure download endpoints with permission checks Notifications & Alerts: - Real-time WebSocket notifications - Push notifications via Firebase integration - @username mention alerts with notification history - Unread message and mention counting - Custom notification types (message, mention, group invite) Advanced Features: - Group chat management with roles (member, admin, owner) - User search and chat member management - Message pagination and chat history - Last seen timestamps and activity tracking - Comprehensive API documentation with WebSocket events Architecture: - Clean layered architecture with services, models, schemas - WebSocket connection manager for real-time features - Modular notification system with multiple channels - Comprehensive error handling and validation - Production-ready with Docker support Technologies: FastAPI, WebSocket, SQLAlchemy, SQLite, Cryptography, Firebase, Pillow
255 lines
8.3 KiB
Python
255 lines
8.3 KiB
Python
import os
|
|
import json
|
|
from typing import Dict, List, Optional
|
|
import firebase_admin
|
|
from firebase_admin import credentials, messaging
|
|
from app.models.user import User
|
|
from app.db.session import SessionLocal
|
|
|
|
class PushNotificationService:
|
|
def __init__(self):
|
|
self.firebase_app = None
|
|
self._initialize_firebase()
|
|
|
|
def _initialize_firebase(self):
|
|
"""Initialize Firebase Admin SDK"""
|
|
try:
|
|
# Check if Firebase credentials are available
|
|
firebase_cred_path = os.getenv("FIREBASE_CREDENTIALS_PATH")
|
|
firebase_cred_json = os.getenv("FIREBASE_CREDENTIALS_JSON")
|
|
|
|
if firebase_cred_path and os.path.exists(firebase_cred_path):
|
|
cred = credentials.Certificate(firebase_cred_path)
|
|
self.firebase_app = firebase_admin.initialize_app(cred)
|
|
elif firebase_cred_json:
|
|
cred_dict = json.loads(firebase_cred_json)
|
|
cred = credentials.Certificate(cred_dict)
|
|
self.firebase_app = firebase_admin.initialize_app(cred)
|
|
else:
|
|
print("Firebase credentials not found. Push notifications will be disabled.")
|
|
self.firebase_app = None
|
|
|
|
except Exception as e:
|
|
print(f"Failed to initialize Firebase: {e}")
|
|
self.firebase_app = None
|
|
|
|
async def send_push_notification(
|
|
self,
|
|
device_token: str,
|
|
title: str,
|
|
body: str,
|
|
data: Optional[Dict[str, str]] = None
|
|
) -> bool:
|
|
"""Send push notification to a device"""
|
|
if not self.firebase_app or not device_token:
|
|
return False
|
|
|
|
try:
|
|
# Create message
|
|
message = messaging.Message(
|
|
notification=messaging.Notification(
|
|
title=title,
|
|
body=body
|
|
),
|
|
data=data or {},
|
|
token=device_token,
|
|
android=messaging.AndroidConfig(
|
|
notification=messaging.AndroidNotification(
|
|
icon="ic_notification",
|
|
color="#2196F3",
|
|
sound="default",
|
|
channel_id="chat_messages"
|
|
),
|
|
priority="high"
|
|
),
|
|
apns=messaging.APNSConfig(
|
|
payload=messaging.APNSPayload(
|
|
aps=messaging.Aps(
|
|
alert=messaging.ApsAlert(
|
|
title=title,
|
|
body=body
|
|
),
|
|
badge=1,
|
|
sound="default"
|
|
)
|
|
)
|
|
)
|
|
)
|
|
|
|
# Send message
|
|
response = messaging.send(message)
|
|
print(f"Successfully sent message: {response}")
|
|
return True
|
|
|
|
except Exception as e:
|
|
print(f"Failed to send push notification: {e}")
|
|
return False
|
|
|
|
async def send_message_notification(
|
|
self,
|
|
recipient_user_id: int,
|
|
sender_username: str,
|
|
message_content: str,
|
|
chat_id: int,
|
|
chat_name: Optional[str] = None
|
|
):
|
|
"""Send push notification for new message"""
|
|
db = SessionLocal()
|
|
try:
|
|
user = db.query(User).filter(User.id == recipient_user_id).first()
|
|
if not user or not user.device_token:
|
|
return
|
|
|
|
title = f"New message from {sender_username}"
|
|
if chat_name:
|
|
title = f"New message in {chat_name}"
|
|
|
|
body = message_content[:100]
|
|
if len(message_content) > 100:
|
|
body += "..."
|
|
|
|
data = {
|
|
"type": "message",
|
|
"chat_id": str(chat_id),
|
|
"sender_username": sender_username,
|
|
"chat_name": chat_name or ""
|
|
}
|
|
|
|
await self.send_push_notification(
|
|
device_token=user.device_token,
|
|
title=title,
|
|
body=body,
|
|
data=data
|
|
)
|
|
|
|
finally:
|
|
db.close()
|
|
|
|
async def send_mention_notification(
|
|
self,
|
|
mentioned_user_id: int,
|
|
sender_username: str,
|
|
message_content: str,
|
|
chat_id: int,
|
|
chat_name: Optional[str] = None
|
|
):
|
|
"""Send push notification for mention"""
|
|
db = SessionLocal()
|
|
try:
|
|
user = db.query(User).filter(User.id == mentioned_user_id).first()
|
|
if not user or not user.device_token:
|
|
return
|
|
|
|
title = f"You were mentioned by {sender_username}"
|
|
if chat_name:
|
|
title = f"Mentioned in {chat_name}"
|
|
|
|
body = message_content[:100]
|
|
if len(message_content) > 100:
|
|
body += "..."
|
|
|
|
data = {
|
|
"type": "mention",
|
|
"chat_id": str(chat_id),
|
|
"sender_username": sender_username,
|
|
"chat_name": chat_name or ""
|
|
}
|
|
|
|
await self.send_push_notification(
|
|
device_token=user.device_token,
|
|
title=title,
|
|
body=body,
|
|
data=data
|
|
)
|
|
|
|
finally:
|
|
db.close()
|
|
|
|
async def send_group_invite_notification(
|
|
self,
|
|
invited_user_id: int,
|
|
inviter_username: str,
|
|
group_name: str,
|
|
chat_id: int
|
|
):
|
|
"""Send push notification for group invitation"""
|
|
db = SessionLocal()
|
|
try:
|
|
user = db.query(User).filter(User.id == invited_user_id).first()
|
|
if not user or not user.device_token:
|
|
return
|
|
|
|
title = f"Invited to {group_name}"
|
|
body = f"{inviter_username} invited you to join {group_name}"
|
|
|
|
data = {
|
|
"type": "group_invite",
|
|
"chat_id": str(chat_id),
|
|
"inviter_username": inviter_username,
|
|
"group_name": group_name
|
|
}
|
|
|
|
await self.send_push_notification(
|
|
device_token=user.device_token,
|
|
title=title,
|
|
body=body,
|
|
data=data
|
|
)
|
|
|
|
finally:
|
|
db.close()
|
|
|
|
async def send_bulk_notifications(
|
|
self,
|
|
tokens_and_messages: List[Dict]
|
|
) -> Dict[str, int]:
|
|
"""Send multiple notifications at once"""
|
|
if not self.firebase_app:
|
|
return {"success": 0, "failure": len(tokens_and_messages)}
|
|
|
|
messages = []
|
|
for item in tokens_and_messages:
|
|
message = messaging.Message(
|
|
notification=messaging.Notification(
|
|
title=item["title"],
|
|
body=item["body"]
|
|
),
|
|
data=item.get("data", {}),
|
|
token=item["token"]
|
|
)
|
|
messages.append(message)
|
|
|
|
try:
|
|
response = messaging.send_all(messages)
|
|
return {
|
|
"success": response.success_count,
|
|
"failure": response.failure_count
|
|
}
|
|
except Exception as e:
|
|
print(f"Failed to send bulk notifications: {e}")
|
|
return {"success": 0, "failure": len(tokens_and_messages)}
|
|
|
|
def validate_device_token(self, device_token: str) -> bool:
|
|
"""Validate if device token is valid"""
|
|
if not self.firebase_app or not device_token:
|
|
return False
|
|
|
|
try:
|
|
# Try to send a test message (dry run)
|
|
message = messaging.Message(
|
|
notification=messaging.Notification(
|
|
title="Test",
|
|
body="Test"
|
|
),
|
|
token=device_token
|
|
)
|
|
|
|
# Dry run - doesn't actually send
|
|
messaging.send(message, dry_run=True)
|
|
return True
|
|
|
|
except Exception:
|
|
return False
|
|
|
|
# Global push notification service instance
|
|
push_notification_service = PushNotificationService() |