mirror of
https://github.com/fromchat-messenger/web.git
synced 2026-09-22 19:15:08 +03:00
Add push debug code
This commit is contained in:
@@ -32,6 +32,12 @@ def _load_firebase_service_account_dict(cert_path: Path) -> dict:
|
|||||||
|
|
||||||
|
|
||||||
class PushNotificationService:
|
class PushNotificationService:
|
||||||
|
def _short_token(self, token: str) -> str:
|
||||||
|
value = (token or "").strip()
|
||||||
|
if len(value) <= 14:
|
||||||
|
return value
|
||||||
|
return f"...{value[-8:]}"
|
||||||
|
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
self.vapid_private_key = os.getenv("VAPID_PRIVATE_KEY")
|
self.vapid_private_key = os.getenv("VAPID_PRIVATE_KEY")
|
||||||
self.vapid_public_key = os.getenv("VAPID_PUBLIC_KEY")
|
self.vapid_public_key = os.getenv("VAPID_PUBLIC_KEY")
|
||||||
@@ -87,16 +93,34 @@ class PushNotificationService:
|
|||||||
|
|
||||||
async def send_public_message_notification(self, db: Session, message: Message, exclude_user_id: Optional[int] = None):
|
async def send_public_message_notification(self, db: Session, message: Message, exclude_user_id: Optional[int] = None):
|
||||||
"""Send push notification for a new public chat message"""
|
"""Send push notification for a new public chat message"""
|
||||||
|
logger.info(
|
||||||
|
"send_public_message_notification start: message_id=%s sender_id=%s exclude_user=%s",
|
||||||
|
message.id,
|
||||||
|
message.user_id,
|
||||||
|
exclude_user_id,
|
||||||
|
)
|
||||||
try:
|
try:
|
||||||
# Get all users except the sender
|
# Get all users except the sender
|
||||||
users = db.query(User).filter(User.id != message.user_id)
|
users = db.query(User).filter(User.id != message.user_id)
|
||||||
if exclude_user_id:
|
if exclude_user_id:
|
||||||
users = users.filter(User.id != exclude_user_id)
|
users = users.filter(User.id != exclude_user_id)
|
||||||
|
user_list = users.all()
|
||||||
|
logger.debug(
|
||||||
|
"send_public_message_notification targets=%s",
|
||||||
|
[user.id for user in user_list],
|
||||||
|
)
|
||||||
|
logger.info(
|
||||||
|
"send_public_message_notification user_count=%s for message_id=%s",
|
||||||
|
len(user_list),
|
||||||
|
message.id,
|
||||||
|
)
|
||||||
|
|
||||||
for user in users:
|
for user in user_list:
|
||||||
# Check if user has push subscription before trying to send
|
# Check if user has push subscription before trying to send
|
||||||
# Try all FCM tokens first (Android). If none or all fail, fall back to web push subscription.
|
# Try all FCM tokens first (Android). If none or all fail, fall back to web push subscription.
|
||||||
fcm_rows = db.query(FcmToken).filter(FcmToken.user_id == user.id).all()
|
fcm_rows = db.query(FcmToken).filter(FcmToken.user_id == user.id).all()
|
||||||
|
if not fcm_rows:
|
||||||
|
logger.debug("No FCM tokens for user %s for message %s", user.id, message.id)
|
||||||
payload_data = {
|
payload_data = {
|
||||||
"type": "public_message",
|
"type": "public_message",
|
||||||
"message_id": message.id,
|
"message_id": message.id,
|
||||||
@@ -105,15 +129,41 @@ class PushNotificationService:
|
|||||||
}
|
}
|
||||||
title = f"{message.author.username}"
|
title = f"{message.author.username}"
|
||||||
body = message.content[:100] + ("..." if len(message.content) > 100 else "")
|
body = message.content[:100] + ("..." if len(message.content) > 100 else "")
|
||||||
|
logger.debug(
|
||||||
|
"send_public_message_notification: user=%s fcm_tokens=%d",
|
||||||
|
user.id,
|
||||||
|
len(fcm_rows),
|
||||||
|
)
|
||||||
|
|
||||||
if fcm_rows and self.firebase_initialized:
|
if fcm_rows and self.firebase_initialized:
|
||||||
for fcm in fcm_rows:
|
for fcm in fcm_rows:
|
||||||
try:
|
try:
|
||||||
self._send_fcm_to_token(fcm.token, title, body, payload_data)
|
response = self._send_fcm_to_token(
|
||||||
|
fcm.token,
|
||||||
|
title,
|
||||||
|
body,
|
||||||
|
payload_data,
|
||||||
|
)
|
||||||
|
logger.info(
|
||||||
|
"FCM public push sent user=%s token=%s response=%s",
|
||||||
|
user.id,
|
||||||
|
self._short_token(fcm.token),
|
||||||
|
response,
|
||||||
|
)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"Failed to send FCM to user {user.id} token {fcm.token}: {e}")
|
logger.error(
|
||||||
|
"Failed to send FCM to user %s token %s: %s",
|
||||||
|
user.id,
|
||||||
|
self._short_token(fcm.token),
|
||||||
|
e,
|
||||||
|
)
|
||||||
# Check if this is a permanent failure and clean up the token
|
# Check if this is a permanent failure and clean up the token
|
||||||
self._cleanup_failed_fcm_token(db, fcm, str(e))
|
self._cleanup_failed_fcm_token(db, fcm, str(e))
|
||||||
|
if fcm_rows and not self.firebase_initialized:
|
||||||
|
logger.warning(
|
||||||
|
"Firebase SDK not initialized, skipped FCM pushes for message %s",
|
||||||
|
message.id,
|
||||||
|
)
|
||||||
|
|
||||||
subscription = db.query(PushSubscription).filter(PushSubscription.user_id == user.id).first()
|
subscription = db.query(PushSubscription).filter(PushSubscription.user_id == user.id).first()
|
||||||
if subscription:
|
if subscription:
|
||||||
@@ -125,6 +175,12 @@ class PushNotificationService:
|
|||||||
|
|
||||||
async def send_dm_notification(self, db: Session, dm_envelope: DMEnvelope, sender: User):
|
async def send_dm_notification(self, db: Session, dm_envelope: DMEnvelope, sender: User):
|
||||||
"""Send push notification for a new DM"""
|
"""Send push notification for a new DM"""
|
||||||
|
logger.info(
|
||||||
|
"send_dm_notification start: dm_id=%s sender_id=%s recipient=%s",
|
||||||
|
dm_envelope.id,
|
||||||
|
sender.id,
|
||||||
|
dm_envelope.recipient_id,
|
||||||
|
)
|
||||||
try:
|
try:
|
||||||
title = f"{sender.username}"
|
title = f"{sender.username}"
|
||||||
body = "New direct message"
|
body = "New direct message"
|
||||||
@@ -136,14 +192,40 @@ class PushNotificationService:
|
|||||||
}
|
}
|
||||||
|
|
||||||
fcm_rows = db.query(FcmToken).filter(FcmToken.user_id == dm_envelope.recipient_id).all()
|
fcm_rows = db.query(FcmToken).filter(FcmToken.user_id == dm_envelope.recipient_id).all()
|
||||||
|
logger.debug(
|
||||||
|
"send_dm_notification: recipient=%s fcm_tokens=%d",
|
||||||
|
dm_envelope.recipient_id,
|
||||||
|
len(fcm_rows),
|
||||||
|
)
|
||||||
if fcm_rows and self.firebase_initialized:
|
if fcm_rows and self.firebase_initialized:
|
||||||
for fcm in fcm_rows:
|
for fcm in fcm_rows:
|
||||||
try:
|
try:
|
||||||
self._send_fcm_to_token(fcm.token, title, body, payload_data)
|
response = self._send_fcm_to_token(
|
||||||
|
fcm.token,
|
||||||
|
title,
|
||||||
|
body,
|
||||||
|
payload_data,
|
||||||
|
)
|
||||||
|
logger.info(
|
||||||
|
"FCM dm push sent recipient=%s token=%s response=%s",
|
||||||
|
dm_envelope.recipient_id,
|
||||||
|
self._short_token(fcm.token),
|
||||||
|
response,
|
||||||
|
)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"Failed to send FCM to user {dm_envelope.recipient_id} token {fcm.token}: {e}")
|
logger.error(
|
||||||
|
"Failed to send FCM to user %s token %s: %s",
|
||||||
|
dm_envelope.recipient_id,
|
||||||
|
self._short_token(fcm.token),
|
||||||
|
e,
|
||||||
|
)
|
||||||
# Check if this is a permanent failure and clean up the token
|
# Check if this is a permanent failure and clean up the token
|
||||||
self._cleanup_failed_fcm_token(db, fcm, str(e))
|
self._cleanup_failed_fcm_token(db, fcm, str(e))
|
||||||
|
if fcm_rows and not self.firebase_initialized:
|
||||||
|
logger.warning(
|
||||||
|
"Firebase SDK not initialized, skipped FCM DM push for dm %s",
|
||||||
|
dm_envelope.id,
|
||||||
|
)
|
||||||
|
|
||||||
await self._send_notification_to_user(
|
await self._send_notification_to_user(
|
||||||
db, dm_envelope.recipient_id, title, body, sender.profile_picture, payload_data
|
db, dm_envelope.recipient_id, title, body, sender.profile_picture, payload_data
|
||||||
@@ -155,6 +237,8 @@ class PushNotificationService:
|
|||||||
"""Send a push notification to a specific user"""
|
"""Send a push notification to a specific user"""
|
||||||
try:
|
try:
|
||||||
subscription = db.query(PushSubscription).filter(PushSubscription.user_id == user_id).first()
|
subscription = db.query(PushSubscription).filter(PushSubscription.user_id == user_id).first()
|
||||||
|
if not subscription:
|
||||||
|
logger.debug("No web push subscription for user %s", user_id)
|
||||||
if not subscription:
|
if not subscription:
|
||||||
return
|
return
|
||||||
|
|
||||||
@@ -180,6 +264,7 @@ class PushNotificationService:
|
|||||||
vapid_private_key=self.vapid_private_key,
|
vapid_private_key=self.vapid_private_key,
|
||||||
vapid_claims=self.vapid_claims
|
vapid_claims=self.vapid_claims
|
||||||
)
|
)
|
||||||
|
logger.info("WebPush sent to user=%s", user_id)
|
||||||
|
|
||||||
except WebPushException as e:
|
except WebPushException as e:
|
||||||
logger.error(f"WebPush error for user {user_id}: {e}")
|
logger.error(f"WebPush error for user {user_id}: {e}")
|
||||||
@@ -210,9 +295,10 @@ class PushNotificationService:
|
|||||||
apns=firebase_messaging.APNSConfig(headers={"apns-priority": "10"})
|
apns=firebase_messaging.APNSConfig(headers={"apns-priority": "10"})
|
||||||
)
|
)
|
||||||
resp = firebase_messaging.send(msg)
|
resp = firebase_messaging.send(msg)
|
||||||
|
logger.debug("Firebase message queued token=%s", self._short_token(token))
|
||||||
return resp
|
return resp
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"Firebase Admin send failed for token {token}: {e}")
|
logger.error("Firebase Admin send failed for token %s: %s", self._short_token(token), e)
|
||||||
raise
|
raise
|
||||||
|
|
||||||
def _cleanup_failed_fcm_token(self, db: Session, fcm_token_entry, error_message: str):
|
def _cleanup_failed_fcm_token(self, db: Session, fcm_token_entry, error_message: str):
|
||||||
@@ -228,13 +314,25 @@ class PushNotificationService:
|
|||||||
is_permanent = any(permanent_error in error_lower for permanent_error in permanent_errors)
|
is_permanent = any(permanent_error in error_lower for permanent_error in permanent_errors)
|
||||||
|
|
||||||
if is_permanent:
|
if is_permanent:
|
||||||
logger.info(f"Removing permanently failed FCM token for user {fcm_token_entry.user_id}: {fcm_token_entry.token}")
|
logger.info(
|
||||||
|
"Removing permanently failed FCM token for user %s: %s",
|
||||||
|
fcm_token_entry.user_id,
|
||||||
|
self._short_token(fcm_token_entry.token),
|
||||||
|
)
|
||||||
db.query(FcmToken).filter(FcmToken.id == fcm_token_entry.id).delete()
|
db.query(FcmToken).filter(FcmToken.id == fcm_token_entry.id).delete()
|
||||||
db.commit()
|
db.commit()
|
||||||
else:
|
else:
|
||||||
logger.debug(f"Temporary FCM failure for token {fcm_token_entry.token}, keeping token: {error_message}")
|
logger.debug(
|
||||||
|
"Temporary FCM failure for token %s, keeping token: %s",
|
||||||
|
self._short_token(fcm_token_entry.token),
|
||||||
|
error_message,
|
||||||
|
)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"Failed to cleanup FCM token {fcm_token_entry.token}: {e}")
|
logger.error(
|
||||||
|
"Failed to cleanup FCM token for user %s: %s",
|
||||||
|
fcm_token_entry.user_id,
|
||||||
|
e,
|
||||||
|
)
|
||||||
try:
|
try:
|
||||||
db.rollback()
|
db.rollback()
|
||||||
except Exception:
|
except Exception:
|
||||||
|
|||||||
@@ -419,6 +419,12 @@ async def _send_message_internal(
|
|||||||
|
|
||||||
# Send push notifications for public messages
|
# Send push notifications for public messages
|
||||||
try:
|
try:
|
||||||
|
logger.info(
|
||||||
|
"Public message saved: id=%s user=%s content_length=%s",
|
||||||
|
new_message.id,
|
||||||
|
current_user.id,
|
||||||
|
len(new_message.content or ""),
|
||||||
|
)
|
||||||
await push_service.send_public_message_notification(db, new_message, exclude_user_id=current_user.id)
|
await push_service.send_public_message_notification(db, new_message, exclude_user_id=current_user.id)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"Failed to send push notification for message {new_message.id}: {e}")
|
logger.error(f"Failed to send push notification for message {new_message.id}: {e}")
|
||||||
@@ -504,7 +510,11 @@ async def register_fcm_token(request: Request, body: RegisterFcmRequest, current
|
|||||||
new = FcmToken(user_id=current_user.id, token=token)
|
new = FcmToken(user_id=current_user.id, token=token)
|
||||||
db.add(new)
|
db.add(new)
|
||||||
db.commit()
|
db.commit()
|
||||||
logger.info(f"Registered FCM token for user {current_user.id}: {token}")
|
logger.info(
|
||||||
|
"Registered FCM token for user %s: ...%s",
|
||||||
|
current_user.id,
|
||||||
|
token[-8:],
|
||||||
|
)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
try:
|
try:
|
||||||
db.rollback()
|
db.rollback()
|
||||||
@@ -521,9 +531,15 @@ async def unregister_fcm_token(request: Request, body: RegisterFcmRequest | None
|
|||||||
Unregister an FCM token. If `body.token` provided, remove only that token for the user.
|
Unregister an FCM token. If `body.token` provided, remove only that token for the user.
|
||||||
If no token provided, remove all tokens for the user.
|
If no token provided, remove all tokens for the user.
|
||||||
"""
|
"""
|
||||||
|
token = body.token.strip() if body and body.token else None
|
||||||
|
logger.info(
|
||||||
|
"Unregister FCM request user=%s token=%s",
|
||||||
|
current_user.id,
|
||||||
|
f"...{token[-8:]}" if token else "ALL",
|
||||||
|
)
|
||||||
try:
|
try:
|
||||||
if body and body.token:
|
if token:
|
||||||
db.query(FcmToken).filter(FcmToken.user_id == current_user.id, FcmToken.token == body.token.strip()).delete()
|
db.query(FcmToken).filter(FcmToken.user_id == current_user.id, FcmToken.token == token).delete()
|
||||||
else:
|
else:
|
||||||
db.query(FcmToken).filter(FcmToken.user_id == current_user.id).delete()
|
db.query(FcmToken).filter(FcmToken.user_id == current_user.id).delete()
|
||||||
db.commit()
|
db.commit()
|
||||||
@@ -546,6 +562,11 @@ async def push_test(request: Request, current_user: User = Depends(get_current_u
|
|||||||
fcm_rows = db.query(FcmToken).filter(FcmToken.user_id == current_user.id).all()
|
fcm_rows = db.query(FcmToken).filter(FcmToken.user_id == current_user.id).all()
|
||||||
if not fcm_rows:
|
if not fcm_rows:
|
||||||
raise HTTPException(status_code=404, detail="No FCM token registered for user")
|
raise HTTPException(status_code=404, detail="No FCM token registered for user")
|
||||||
|
logger.info(
|
||||||
|
"push_test start: user=%s token_count=%s",
|
||||||
|
current_user.id,
|
||||||
|
len(fcm_rows),
|
||||||
|
)
|
||||||
|
|
||||||
title = "FromChat test"
|
title = "FromChat test"
|
||||||
body = "This is a test push from the server"
|
body = "This is a test push from the server"
|
||||||
@@ -555,9 +576,20 @@ async def push_test(request: Request, current_user: User = Depends(get_current_u
|
|||||||
failures = []
|
failures = []
|
||||||
for fcm in fcm_rows:
|
for fcm in fcm_rows:
|
||||||
try:
|
try:
|
||||||
push_service._send_fcm_to_token(fcm.token, title, body, data)
|
response = push_service._send_fcm_to_token(fcm.token, title, body, data)
|
||||||
|
logger.info(
|
||||||
|
"push_test sent user=%s token=%s response=%s",
|
||||||
|
current_user.id,
|
||||||
|
f"{fcm.token[-8:]}",
|
||||||
|
response,
|
||||||
|
)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"Failed to send test push to user {current_user.id} token {fcm.token}: {e}")
|
logger.error(
|
||||||
|
"Failed to send test push to user %s token %s: %s",
|
||||||
|
current_user.id,
|
||||||
|
f"...{fcm.token[-8:]}",
|
||||||
|
e,
|
||||||
|
)
|
||||||
failures.append(str(e))
|
failures.append(str(e))
|
||||||
|
|
||||||
if failures and len(failures) == len(fcm_rows):
|
if failures and len(failures) == len(fcm_rows):
|
||||||
|
|||||||
Reference in New Issue
Block a user