fix(notification+vlog upload)
This commit is contained in:
@@ -9,6 +9,7 @@ from models.notification import Notification, NotificationType
|
||||
from schemas.event import EventCreate, EventUpdate, EventResponse, ParticipationUpdate
|
||||
from utils.security import get_current_active_user
|
||||
from utils.email import send_event_notification
|
||||
from utils.push_service import send_push_to_user
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
@@ -48,15 +49,22 @@ async def create_event(
|
||||
|
||||
# Create notification
|
||||
if user.id != current_user.id:
|
||||
notif_title = f"Invitation à un événement privé: {event.title}"
|
||||
notif_message = f"{current_user.full_name} vous a invité à un événement privé"
|
||||
notif_link = f"/events/{event.id}"
|
||||
|
||||
notification = Notification(
|
||||
user_id=user.id,
|
||||
type=NotificationType.EVENT_INVITATION,
|
||||
title=f"Invitation à un événement privé: {event.title}",
|
||||
message=f"{current_user.full_name} vous a invité à un événement privé",
|
||||
link=f"/events/{event.id}"
|
||||
title=notif_title,
|
||||
message=notif_message,
|
||||
link=notif_link
|
||||
)
|
||||
db.add(notification)
|
||||
|
||||
# Send push notification
|
||||
send_push_to_user(db, user.id, notif_title, notif_message, notif_link)
|
||||
|
||||
# Send email notification
|
||||
try:
|
||||
send_event_notification(user.email, event)
|
||||
@@ -75,15 +83,22 @@ async def create_event(
|
||||
|
||||
# Create notification
|
||||
if user.id != current_user.id:
|
||||
notif_title = f"Nouvel événement: {event.title}"
|
||||
notif_message = f"{current_user.full_name} a créé un nouvel événement"
|
||||
notif_link = f"/events/{event.id}"
|
||||
|
||||
notification = Notification(
|
||||
user_id=user.id,
|
||||
type=NotificationType.EVENT_INVITATION,
|
||||
title=f"Nouvel événement: {event.title}",
|
||||
message=f"{current_user.full_name} a créé un nouvel événement",
|
||||
link=f"/events/{event.id}"
|
||||
title=notif_title,
|
||||
message=notif_message,
|
||||
link=notif_link
|
||||
)
|
||||
db.add(notification)
|
||||
|
||||
# Send push notification
|
||||
send_push_to_user(db, user.id, notif_title, notif_message, notif_link)
|
||||
|
||||
# Send email notification
|
||||
try:
|
||||
send_event_notification(user.email, event)
|
||||
@@ -309,15 +324,22 @@ async def invite_users_to_event(
|
||||
db.add(participation)
|
||||
|
||||
# Créer une notification
|
||||
notif_title = f"Invitation à un événement privé: {event.title}"
|
||||
notif_message = f"{current_user.full_name} vous a invité à un événement privé"
|
||||
notif_link = f"/events/{event.id}"
|
||||
|
||||
notification = Notification(
|
||||
user_id=user.id,
|
||||
type=NotificationType.EVENT_INVITATION,
|
||||
title=f"Invitation à un événement privé: {event.title}",
|
||||
message=f"{current_user.full_name} vous a invité à un événement privé",
|
||||
link=f"/events/{event.id}"
|
||||
title=notif_title,
|
||||
message=notif_message,
|
||||
link=notif_link
|
||||
)
|
||||
db.add(notification)
|
||||
|
||||
# Send push notification
|
||||
send_push_to_user(db, user.id, notif_title, notif_message, notif_link)
|
||||
|
||||
# Envoyer un email
|
||||
try:
|
||||
send_event_notification(user.email, event)
|
||||
|
||||
67
backend/api/routers/push.py
Normal file
67
backend/api/routers/push.py
Normal file
@@ -0,0 +1,67 @@
|
||||
from fastapi import APIRouter, Depends, HTTPException, status
|
||||
from sqlalchemy.orm import Session
|
||||
from config.database import get_db
|
||||
from config.settings import settings
|
||||
from models.notification import PushSubscription
|
||||
from models.user import User
|
||||
from schemas.notification import PushSubscriptionCreate, VapidPublicKeyResponse
|
||||
from utils.security import get_current_active_user
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
@router.get("/vapid-public-key", response_model=VapidPublicKeyResponse)
|
||||
async def get_vapid_public_key(current_user: User = Depends(get_current_active_user)):
|
||||
"""Get the VAPID public key for push notifications."""
|
||||
if not settings.VAPID_PUBLIC_KEY:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_503_SERVICE_UNAVAILABLE,
|
||||
detail="VAPID keys not configured on server"
|
||||
)
|
||||
return {"public_key": settings.VAPID_PUBLIC_KEY}
|
||||
|
||||
@router.post("/subscribe")
|
||||
async def subscribe_push(
|
||||
subscription: PushSubscriptionCreate,
|
||||
db: Session = Depends(get_db),
|
||||
current_user: User = Depends(get_current_active_user)
|
||||
):
|
||||
"""Subscribe to push notifications."""
|
||||
# Check if subscription already exists
|
||||
existing = db.query(PushSubscription).filter(
|
||||
PushSubscription.endpoint == subscription.endpoint
|
||||
).first()
|
||||
|
||||
if existing:
|
||||
# Update existing subscription
|
||||
existing.user_id = current_user.id
|
||||
existing.p256dh = subscription.keys.p256dh
|
||||
existing.auth = subscription.keys.auth
|
||||
db.commit()
|
||||
return {"message": "Subscription updated"}
|
||||
|
||||
# Create new subscription
|
||||
new_sub = PushSubscription(
|
||||
user_id=current_user.id,
|
||||
endpoint=subscription.endpoint,
|
||||
p256dh=subscription.keys.p256dh,
|
||||
auth=subscription.keys.auth
|
||||
)
|
||||
db.add(new_sub)
|
||||
db.commit()
|
||||
|
||||
return {"message": "Subscribed successfully"}
|
||||
|
||||
@router.delete("/unsubscribe")
|
||||
async def unsubscribe_push(
|
||||
endpoint: str,
|
||||
db: Session = Depends(get_db),
|
||||
current_user: User = Depends(get_current_active_user)
|
||||
):
|
||||
"""Unsubscribe from push notifications."""
|
||||
db.query(PushSubscription).filter(
|
||||
PushSubscription.endpoint == endpoint
|
||||
).delete()
|
||||
db.commit()
|
||||
|
||||
return {"message": "Unsubscribed successfully"}
|
||||
|
||||
@@ -13,6 +13,7 @@ from schemas.vlog import VlogCreate, VlogUpdate, VlogResponse, VlogCommentCreate
|
||||
from utils.security import get_current_active_user
|
||||
from utils.video_utils import generate_video_thumbnail, get_video_duration
|
||||
from utils.settings_service import SettingsService
|
||||
from utils.push_service import send_push_to_user
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
@@ -254,12 +255,15 @@ async def upload_vlog_video(
|
||||
"""Upload a vlog video."""
|
||||
# Validate video file
|
||||
if not SettingsService.is_file_type_allowed(db, video.content_type or "unknown"):
|
||||
allowed_types = SettingsService.get_setting(db, "allowed_video_types",
|
||||
["video/mp4", "video/mpeg", "video/quicktime", "video/webm"])
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail=f"Invalid video type. Allowed types: {', '.join(allowed_types)}"
|
||||
)
|
||||
# Fallback check for common video types if content_type is generic application/octet-stream
|
||||
filename = video.filename.lower()
|
||||
if not (filename.endswith('.mp4') or filename.endswith('.mov') or filename.endswith('.webm') or filename.endswith('.mkv')):
|
||||
allowed_types = SettingsService.get_setting(db, "allowed_video_types",
|
||||
["video/mp4", "video/mpeg", "video/quicktime", "video/webm"])
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail=f"Invalid video type. Allowed types: {', '.join(allowed_types)}"
|
||||
)
|
||||
|
||||
# Check file size
|
||||
video_content = await video.read()
|
||||
@@ -335,17 +339,24 @@ async def upload_vlog_video(
|
||||
|
||||
# Create notifications for all active users (except the creator)
|
||||
users = db.query(User).filter(User.is_active == True).all()
|
||||
notif_title = "Nouveau vlog"
|
||||
notif_message = f"{current_user.full_name} a publié un nouveau vlog : {vlog.title}"
|
||||
notif_link = f"/vlogs/{vlog.id}"
|
||||
|
||||
for user in users:
|
||||
if user.id != current_user.id:
|
||||
notification = Notification(
|
||||
user_id=user.id,
|
||||
type=NotificationType.NEW_VLOG,
|
||||
title="Nouveau vlog",
|
||||
message=f"{current_user.full_name} a publié un nouveau vlog : {vlog.title}",
|
||||
link=f"/vlogs/{vlog.id}",
|
||||
title=notif_title,
|
||||
message=notif_message,
|
||||
link=notif_link,
|
||||
is_read=False
|
||||
)
|
||||
db.add(notification)
|
||||
|
||||
# Envoyer la notification push
|
||||
send_push_to_user(db, user.id, notif_title, notif_message, notif_link)
|
||||
|
||||
db.commit()
|
||||
|
||||
|
||||
@@ -8,7 +8,7 @@ import mimetypes
|
||||
|
||||
from config.settings import settings
|
||||
from config.database import engine, Base
|
||||
from api.routers import auth, users, events, albums, posts, vlogs, stats, admin, notifications, settings as settings_router, information, tickets
|
||||
from api.routers import auth, users, events, albums, posts, vlogs, stats, admin, notifications, settings as settings_router, information, tickets, push
|
||||
from utils.init_db import init_database
|
||||
from utils.settings_service import SettingsService
|
||||
from config.database import SessionLocal
|
||||
@@ -294,6 +294,7 @@ app.include_router(vlogs.router, prefix="/api/vlogs", tags=["Vlogs"])
|
||||
app.include_router(stats.router, prefix="/api/stats", tags=["Statistics"])
|
||||
app.include_router(admin.router, prefix="/api/admin", tags=["Admin"])
|
||||
app.include_router(notifications.router, prefix="/api/notifications", tags=["Notifications"])
|
||||
app.include_router(push.router, prefix="/api/push", tags=["Push Notifications"])
|
||||
app.include_router(settings_router.router, prefix="/api/settings", tags=["Settings"])
|
||||
app.include_router(information.router, prefix="/api/information", tags=["Information"])
|
||||
app.include_router(tickets.router, prefix="/api/tickets", tags=["Tickets"])
|
||||
|
||||
@@ -55,6 +55,11 @@ class Settings:
|
||||
if not ADMIN_PASSWORD:
|
||||
raise ValueError("ADMIN_PASSWORD variable is required")
|
||||
|
||||
# Notifications Push (VAPID)
|
||||
VAPID_PRIVATE_KEY: str = os.getenv("VAPID_PRIVATE_KEY", "")
|
||||
VAPID_PUBLIC_KEY: str = os.getenv("VAPID_PUBLIC_KEY", "")
|
||||
VAPID_CLAIMS_EMAIL: str = os.getenv("VAPID_CLAIMS_EMAIL", "mailto:admin@lediscord.com")
|
||||
|
||||
# App
|
||||
APP_NAME: str = os.getenv("APP_NAME", "LeDiscord")
|
||||
APP_URL: str = os.getenv("APP_URL", "http://localhost:5173")
|
||||
|
||||
38
backend/migrations/versions/0002_add_push_subscriptions.py
Normal file
38
backend/migrations/versions/0002_add_push_subscriptions.py
Normal file
@@ -0,0 +1,38 @@
|
||||
"""Add push_subscriptions table for Web Push notifications
|
||||
|
||||
Revision ID: 0002_push_subscriptions
|
||||
Revises: 89527c8da8e1
|
||||
Create Date: 2025-01-27
|
||||
|
||||
"""
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision = '0002_push_subscriptions'
|
||||
down_revision = '89527c8da8e1'
|
||||
branch_labels = None
|
||||
depends_on = None
|
||||
|
||||
|
||||
def upgrade():
|
||||
op.create_table(
|
||||
'push_subscriptions',
|
||||
sa.Column('id', sa.Integer(), nullable=False),
|
||||
sa.Column('user_id', sa.Integer(), nullable=False),
|
||||
sa.Column('endpoint', sa.String(), nullable=False),
|
||||
sa.Column('p256dh', sa.String(), nullable=False),
|
||||
sa.Column('auth', sa.String(), nullable=False),
|
||||
sa.Column('created_at', sa.DateTime(), nullable=True),
|
||||
sa.ForeignKeyConstraint(['user_id'], ['users.id'], ),
|
||||
sa.PrimaryKeyConstraint('id'),
|
||||
sa.UniqueConstraint('endpoint')
|
||||
)
|
||||
op.create_index(op.f('ix_push_subscriptions_id'), 'push_subscriptions', ['id'], unique=False)
|
||||
|
||||
|
||||
def downgrade():
|
||||
op.drop_index(op.f('ix_push_subscriptions_id'), table_name='push_subscriptions')
|
||||
op.drop_table('push_subscriptions')
|
||||
|
||||
@@ -27,3 +27,16 @@ class Notification(Base):
|
||||
|
||||
# Relationships
|
||||
user = relationship("User", back_populates="notifications")
|
||||
|
||||
class PushSubscription(Base):
|
||||
__tablename__ = "push_subscriptions"
|
||||
|
||||
id = Column(Integer, primary_key=True, index=True)
|
||||
user_id = Column(Integer, ForeignKey("users.id"), nullable=False)
|
||||
endpoint = Column(String, unique=True, nullable=False)
|
||||
p256dh = Column(String, nullable=False)
|
||||
auth = Column(String, nullable=False)
|
||||
created_at = Column(DateTime, default=datetime.utcnow)
|
||||
|
||||
# Relationships
|
||||
user = relationship("User", back_populates="push_subscriptions")
|
||||
|
||||
@@ -27,6 +27,7 @@ class User(Base):
|
||||
mentions = relationship("PostMention", back_populates="mentioned_user", cascade="all, delete-orphan")
|
||||
vlogs = relationship("Vlog", back_populates="author", cascade="all, delete-orphan")
|
||||
notifications = relationship("Notification", back_populates="user", cascade="all, delete-orphan")
|
||||
push_subscriptions = relationship("PushSubscription", back_populates="user", cascade="all, delete-orphan")
|
||||
vlog_likes = relationship("VlogLike", back_populates="user", cascade="all, delete-orphan")
|
||||
vlog_comments = relationship("VlogComment", back_populates="user", cascade="all, delete-orphan")
|
||||
media_likes = relationship("MediaLike", back_populates="user", cascade="all, delete-orphan")
|
||||
|
||||
@@ -1,22 +1,15 @@
|
||||
fastapi==0.104.1
|
||||
uvicorn[standard]==0.24.0
|
||||
sqlalchemy==2.0.23
|
||||
alembic==1.12.1
|
||||
psycopg2-binary==2.9.9
|
||||
python-jose[cryptography]==3.3.0
|
||||
passlib[bcrypt]==1.7.4
|
||||
bcrypt==4.0.1
|
||||
python-multipart==0.0.6
|
||||
python-dotenv==1.0.0
|
||||
pydantic==2.5.0
|
||||
pydantic[email]==2.5.0
|
||||
pydantic-settings==2.1.0
|
||||
aiofiles==23.2.1
|
||||
pillow==10.1.0
|
||||
httpx==0.25.2
|
||||
redis==5.0.1
|
||||
celery==5.3.4
|
||||
flower==2.0.1
|
||||
python-magic==0.4.27
|
||||
numpy==1.26.4
|
||||
opencv-python==4.8.1.78
|
||||
fastapi>=0.68.0
|
||||
uvicorn>=0.15.0
|
||||
sqlalchemy>=1.4.0
|
||||
psycopg2-binary>=2.9.0
|
||||
python-jose[cryptography]>=3.3.0
|
||||
passlib[bcrypt]>=1.7.4
|
||||
python-multipart>=0.0.5
|
||||
python-dotenv>=0.19.0
|
||||
pydantic>=1.8.0
|
||||
email-validator>=1.1.3
|
||||
pillow>=9.0.0
|
||||
moviepy>=1.0.3
|
||||
aiofiles>=0.8.0
|
||||
python-magic>=0.4.27
|
||||
pywebpush>=1.14.0
|
||||
|
||||
@@ -15,3 +15,14 @@ class NotificationResponse(BaseModel):
|
||||
|
||||
class Config:
|
||||
from_attributes = True
|
||||
|
||||
class PushSubscriptionKeys(BaseModel):
|
||||
p256dh: str
|
||||
auth: str
|
||||
|
||||
class PushSubscriptionCreate(BaseModel):
|
||||
endpoint: str
|
||||
keys: PushSubscriptionKeys
|
||||
|
||||
class VapidPublicKeyResponse(BaseModel):
|
||||
public_key: str
|
||||
|
||||
136
backend/utils/push_service.py
Normal file
136
backend/utils/push_service.py
Normal file
@@ -0,0 +1,136 @@
|
||||
"""
|
||||
Service d'envoi de notifications push via Web Push (VAPID).
|
||||
"""
|
||||
from typing import Optional, Dict, Any
|
||||
from sqlalchemy.orm import Session
|
||||
from config.settings import settings
|
||||
from models.notification import PushSubscription
|
||||
|
||||
# Import conditionnel de pywebpush
|
||||
try:
|
||||
from pywebpush import webpush, WebPushException
|
||||
WEBPUSH_AVAILABLE = True
|
||||
except ImportError:
|
||||
WEBPUSH_AVAILABLE = False
|
||||
print("⚠️ pywebpush non installé - Les notifications push sont désactivées")
|
||||
|
||||
|
||||
def is_push_configured() -> bool:
|
||||
"""Vérifie si les notifications push sont configurées."""
|
||||
return (
|
||||
WEBPUSH_AVAILABLE and
|
||||
bool(settings.VAPID_PRIVATE_KEY) and
|
||||
bool(settings.VAPID_PUBLIC_KEY)
|
||||
)
|
||||
|
||||
|
||||
def send_push_to_user(
|
||||
db: Session,
|
||||
user_id: int,
|
||||
title: str,
|
||||
body: str,
|
||||
link: str = "/",
|
||||
data: Optional[Dict[str, Any]] = None
|
||||
) -> int:
|
||||
"""
|
||||
Envoie une notification push à tous les appareils d'un utilisateur.
|
||||
|
||||
Args:
|
||||
db: Session de base de données
|
||||
user_id: ID de l'utilisateur
|
||||
title: Titre de la notification
|
||||
body: Corps de la notification
|
||||
link: Lien vers lequel rediriger
|
||||
data: Données supplémentaires
|
||||
|
||||
Returns:
|
||||
Nombre de notifications envoyées avec succès
|
||||
"""
|
||||
if not is_push_configured():
|
||||
return 0
|
||||
|
||||
# Récupérer tous les abonnements de l'utilisateur
|
||||
subscriptions = db.query(PushSubscription).filter(
|
||||
PushSubscription.user_id == user_id
|
||||
).all()
|
||||
|
||||
if not subscriptions:
|
||||
return 0
|
||||
|
||||
success_count = 0
|
||||
failed_endpoints = []
|
||||
|
||||
import json
|
||||
payload = json.dumps({
|
||||
"title": title,
|
||||
"body": body,
|
||||
"link": link,
|
||||
"data": data or {}
|
||||
})
|
||||
|
||||
vapid_claims = {
|
||||
"sub": settings.VAPID_CLAIMS_EMAIL
|
||||
}
|
||||
|
||||
for sub in subscriptions:
|
||||
try:
|
||||
webpush(
|
||||
subscription_info={
|
||||
"endpoint": sub.endpoint,
|
||||
"keys": {
|
||||
"p256dh": sub.p256dh,
|
||||
"auth": sub.auth
|
||||
}
|
||||
},
|
||||
data=payload,
|
||||
vapid_private_key=settings.VAPID_PRIVATE_KEY,
|
||||
vapid_claims=vapid_claims
|
||||
)
|
||||
success_count += 1
|
||||
print(f"✅ Push envoyé à {sub.endpoint[:50]}...")
|
||||
except WebPushException as e:
|
||||
print(f"❌ Erreur push pour {sub.endpoint[:50]}...: {e}")
|
||||
# Si l'abonnement est expiré ou invalide, on le marque pour suppression
|
||||
if e.response and e.response.status_code in [404, 410]:
|
||||
failed_endpoints.append(sub.endpoint)
|
||||
except Exception as e:
|
||||
print(f"❌ Erreur inattendue push: {type(e).__name__}: {e}")
|
||||
|
||||
# Supprimer les abonnements invalides
|
||||
if failed_endpoints:
|
||||
db.query(PushSubscription).filter(
|
||||
PushSubscription.endpoint.in_(failed_endpoints)
|
||||
).delete(synchronize_session=False)
|
||||
db.commit()
|
||||
print(f"🗑️ Supprimé {len(failed_endpoints)} abonnements invalides")
|
||||
|
||||
return success_count
|
||||
|
||||
|
||||
def send_push_to_users(
|
||||
db: Session,
|
||||
user_ids: list,
|
||||
title: str,
|
||||
body: str,
|
||||
link: str = "/",
|
||||
data: Optional[Dict[str, Any]] = None
|
||||
) -> int:
|
||||
"""
|
||||
Envoie une notification push à plusieurs utilisateurs.
|
||||
|
||||
Args:
|
||||
db: Session de base de données
|
||||
user_ids: Liste des IDs utilisateurs
|
||||
title: Titre de la notification
|
||||
body: Corps de la notification
|
||||
link: Lien vers lequel rediriger
|
||||
data: Données supplémentaires
|
||||
|
||||
Returns:
|
||||
Nombre total de notifications envoyées avec succès
|
||||
"""
|
||||
total = 0
|
||||
for user_id in user_ids:
|
||||
total += send_push_to_user(db, user_id, title, body, link, data)
|
||||
return total
|
||||
|
||||
Reference in New Issue
Block a user