diff --git a/src/main/java/com/hcmus/forumus_backend/controller/NotificationController.java b/src/main/java/com/hcmus/forumus_backend/controller/NotificationController.java new file mode 100644 index 0000000..d67a567 --- /dev/null +++ b/src/main/java/com/hcmus/forumus_backend/controller/NotificationController.java @@ -0,0 +1,28 @@ +package com.hcmus.forumus_backend.controller; + +import com.hcmus.forumus_backend.dto.notification.NotificationTriggerRequest; +import com.hcmus.forumus_backend.service.NotificationService; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.*; + +@RestController +@RequestMapping("/api/notifications") +@CrossOrigin +public class NotificationController { + + private final NotificationService notificationService; + + public NotificationController(NotificationService notificationService) { + this.notificationService = notificationService; + } + + @PostMapping("") + public ResponseEntity triggerNotification(@RequestBody NotificationTriggerRequest request) { + boolean success = notificationService.triggerNotification(request); + if (success) { + return ResponseEntity.ok("Notification triggered successfully"); + } else { + return ResponseEntity.badRequest().body("Failed to trigger notification"); + } + } +} diff --git a/src/main/java/com/hcmus/forumus_backend/dto/notification/NotificationTriggerRequest.java b/src/main/java/com/hcmus/forumus_backend/dto/notification/NotificationTriggerRequest.java new file mode 100644 index 0000000..b8f49f4 --- /dev/null +++ b/src/main/java/com/hcmus/forumus_backend/dto/notification/NotificationTriggerRequest.java @@ -0,0 +1,70 @@ +package com.hcmus.forumus_backend.dto.notification; + +public class NotificationTriggerRequest { + private String type; // UPVOTE, COMMENT, REPLY + private String actorId; + private String actorName; + private String targetId; // Post ID or Comment ID + private String targetUserId; // The user to notify + private String previewText; // Snippet of post title or comment + + public NotificationTriggerRequest() { + } + + public NotificationTriggerRequest(String type, String actorId, String actorName, String targetId, String targetUserId, String previewText) { + this.type = type; + this.actorId = actorId; + this.actorName = actorName; + this.targetId = targetId; + this.targetUserId = targetUserId; + this.previewText = previewText; + } + + public String getType() { + return type; + } + + public void setType(String type) { + this.type = type; + } + + public String getActorId() { + return actorId; + } + + public void setActorId(String actorId) { + this.actorId = actorId; + } + + public String getActorName() { + return actorName; + } + + public void setActorName(String actorName) { + this.actorName = actorName; + } + + public String getTargetId() { + return targetId; + } + + public void setTargetId(String targetId) { + this.targetId = targetId; + } + + public String getTargetUserId() { + return targetUserId; + } + + public void setTargetUserId(String targetUserId) { + this.targetUserId = targetUserId; + } + + public String getPreviewText() { + return previewText; + } + + public void setPreviewText(String previewText) { + this.previewText = previewText; + } +} diff --git a/src/main/java/com/hcmus/forumus_backend/service/FCMService.java b/src/main/java/com/hcmus/forumus_backend/service/FCMService.java index 1ee523e..2c1c52a 100644 --- a/src/main/java/com/hcmus/forumus_backend/service/FCMService.java +++ b/src/main/java/com/hcmus/forumus_backend/service/FCMService.java @@ -177,4 +177,52 @@ public void sendMulticastNotification( logger.error("Failed to send multicast notification", e); } } + /** + * Send a general notification to a specific device + * + * @param fcmToken The recipient's FCM token + * @param title The notification title + * @param body The notification body + * @param data Additional data payload + * @return true if sent successfully, false otherwise + */ + public boolean sendGeneralNotification(String fcmToken, String title, String body, Map data) { + try { + // Create notification + Notification notification = Notification.builder() + .setTitle(title) + .setBody(body) + .build(); + + // Build the message + Message.Builder messageBuilder = Message.builder() + .setToken(fcmToken) + .setNotification(notification) + .setAndroidConfig(AndroidConfig.builder() + .setPriority(AndroidConfig.Priority.HIGH) + .setNotification(AndroidNotification.builder() + .setSound("default") + .setChannelId("general_notifications") + .build()) + .build()); + + if (data != null && !data.isEmpty()) { + messageBuilder.putAllData(data); + } + + Message message = messageBuilder.build(); + + // Send the message + String response = FirebaseMessaging.getInstance().send(message); + logger.info("Successfully sent general notification: {}", response); + return true; + + } catch (FirebaseMessagingException e) { + logger.error("Failed to send general notification to token: {}", fcmToken, e); + return false; + } catch (Exception e) { + logger.error("Unexpected error sending general notification", e); + return false; + } + } } diff --git a/src/main/java/com/hcmus/forumus_backend/service/NotificationService.java b/src/main/java/com/hcmus/forumus_backend/service/NotificationService.java new file mode 100644 index 0000000..f7ddced --- /dev/null +++ b/src/main/java/com/hcmus/forumus_backend/service/NotificationService.java @@ -0,0 +1,120 @@ +package com.hcmus.forumus_backend.service; + +import com.google.cloud.Timestamp; +import com.google.cloud.firestore.Firestore; +import com.hcmus.forumus_backend.dto.notification.NotificationTriggerRequest; +import com.hcmus.forumus_backend.model.User; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.stereotype.Service; + +import java.util.HashMap; +import java.util.Map; +import java.util.UUID; +import java.util.concurrent.ExecutionException; + +@Service +public class NotificationService { + + private static final Logger logger = LoggerFactory.getLogger(NotificationService.class); + + private final Firestore db; + private final UserService userService; + private final FCMService fcmService; + + public NotificationService(Firestore db, UserService userService, FCMService fcmService) { + this.db = db; + this.userService = userService; + this.fcmService = fcmService; + } + + public boolean triggerNotification(NotificationTriggerRequest request) { + try { + // 1. Validate inputs + if (request.getTargetUserId() == null || request.getTargetUserId().isEmpty()) { + logger.warn("Notification validation failed: targetUserId is missing"); + return false; + } + + // Don't notify if actor is the same as target user (self-action) + if (request.getTargetUserId().equals(request.getActorId())) { + logger.info("Skipping notification: Actor is target user"); + return true; // Use true because it's not an error, just logic + } + + // 2. Fetch target user to get FCM token + User targetUser; + try { + targetUser = userService.getUserById(request.getTargetUserId()); + } catch (Exception e) { + logger.error("Failed to fetch target user: {}", request.getTargetUserId(), e); + return false; + } + + // 3. Prepare Notification Data for Firestore + String notificationId = UUID.randomUUID().toString(); + Map notificationData = new HashMap<>(); + notificationData.put("id", notificationId); + notificationData.put("type", request.getType()); + notificationData.put("actorId", request.getActorId()); + notificationData.put("actorName", request.getActorName()); + notificationData.put("targetId", request.getTargetId()); + notificationData.put("previewText", request.getPreviewText()); + notificationData.put("createdAt", Timestamp.now()); + notificationData.put("isRead", false); + + // 4. Write to Firestore: users/{targetUserId}/notifications/{notificationId} + db.collection("users") + .document(request.getTargetUserId()) + .collection("notifications") + .document(notificationId) + .set(notificationData); + + logger.info("Notification saved to Firestore: {}", notificationId); + + // 5. Send FCM Push Notification + if (targetUser.getFcmToken() != null && !targetUser.getFcmToken().isEmpty()) { + String title = generateNotificationTitle(request); + String body = generateNotificationBody(request); + + Map data = new HashMap<>(); + data.put("type", "general_notification"); + data.put("notificationId", notificationId); + data.put("targetId", request.getTargetId()); // Post/Comment ID for deep link + data.put("click_action", "FLUTTER_NOTIFICATION_CLICK"); // Standard, but we handle intent in Android + + fcmService.sendGeneralNotification(targetUser.getFcmToken(), title, body, data); + } else { + logger.info("Target user has no FCM token, skipping push notification"); + } + + return true; + + } catch (Exception e) { + logger.error("Error triggering notification", e); + return false; + } + } + + private String generateNotificationTitle(NotificationTriggerRequest request) { + return switch (request.getType()) { + case "UPVOTE" -> "New Upvote"; + case "COMMENT" -> "New Comment"; + case "REPLY" -> "New Reply"; + default -> "New Notification"; + }; + } + + private String generateNotificationBody(NotificationTriggerRequest request) { + String actor = request.getActorName() != null ? request.getActorName() : "Someone"; + String preview = request.getPreviewText() != null ? request.getPreviewText() : ""; + if (preview.length() > 50) preview = preview.substring(0, 50) + "..."; + + return switch (request.getType()) { + case "UPVOTE" -> actor + " upvoted your post: " + preview; + case "COMMENT" -> actor + " commented on your post: " + preview; + case "REPLY" -> actor + " replied to your comment: " + preview; + default -> actor + " interacted with your content."; + }; + } +}