Notification System
This commit is contained in:
parent
3ec22c4770
commit
2bc865fefd
8 changed files with 777 additions and 0 deletions
|
|
@ -12,6 +12,7 @@ import org.operaton.fitpub.repository.ActivityRepository;
|
|||
import org.operaton.fitpub.repository.CommentRepository;
|
||||
import org.operaton.fitpub.repository.UserRepository;
|
||||
import org.operaton.fitpub.service.FederationService;
|
||||
import org.operaton.fitpub.service.NotificationService;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.data.domain.Page;
|
||||
import org.springframework.data.domain.PageRequest;
|
||||
|
|
@ -42,6 +43,7 @@ public class CommentController {
|
|||
private final ActivityRepository activityRepository;
|
||||
private final UserRepository userRepository;
|
||||
private final FederationService federationService;
|
||||
private final NotificationService notificationService;
|
||||
|
||||
@Value("${fitpub.base-url}")
|
||||
private String baseUrl;
|
||||
|
|
@ -130,6 +132,10 @@ public class CommentController {
|
|||
|
||||
log.info("User {} commented on activity {}", user.getUsername(), activityId);
|
||||
|
||||
// Create notification for activity owner
|
||||
String commenterActorUri = user.getActorUri(baseUrl);
|
||||
notificationService.createActivityCommentedNotification(activity, saved, commenterActorUri);
|
||||
|
||||
// Send ActivityPub Create/Note activity to followers if activity is public
|
||||
if (activity.getVisibility() == Activity.Visibility.PUBLIC ||
|
||||
activity.getVisibility() == Activity.Visibility.FOLLOWERS) {
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@ import org.operaton.fitpub.repository.ActivityRepository;
|
|||
import org.operaton.fitpub.repository.LikeRepository;
|
||||
import org.operaton.fitpub.repository.UserRepository;
|
||||
import org.operaton.fitpub.service.FederationService;
|
||||
import org.operaton.fitpub.service.NotificationService;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
|
|
@ -39,6 +40,7 @@ public class LikeController {
|
|||
private final ActivityRepository activityRepository;
|
||||
private final UserRepository userRepository;
|
||||
private final FederationService federationService;
|
||||
private final NotificationService notificationService;
|
||||
|
||||
@Value("${fitpub.base-url}")
|
||||
private String baseUrl;
|
||||
|
|
@ -111,6 +113,10 @@ public class LikeController {
|
|||
|
||||
log.info("User {} liked activity {}", user.getUsername(), activityId);
|
||||
|
||||
// Create notification for activity owner
|
||||
String likerActorUri = user.getActorUri(baseUrl);
|
||||
notificationService.createActivityLikedNotification(activity, likerActorUri);
|
||||
|
||||
// Send ActivityPub Like activity to followers if activity is public
|
||||
if (activity.getVisibility() == Activity.Visibility.PUBLIC) {
|
||||
String activityUri = baseUrl + "/activities/" + activityId;
|
||||
|
|
|
|||
|
|
@ -0,0 +1,159 @@
|
|||
package org.operaton.fitpub.controller;
|
||||
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.operaton.fitpub.model.dto.NotificationDTO;
|
||||
import org.operaton.fitpub.model.entity.Notification;
|
||||
import org.operaton.fitpub.model.entity.User;
|
||||
import org.operaton.fitpub.repository.UserRepository;
|
||||
import org.operaton.fitpub.service.NotificationService;
|
||||
import org.springframework.data.domain.Page;
|
||||
import org.springframework.data.domain.Pageable;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.security.core.annotation.AuthenticationPrincipal;
|
||||
import org.springframework.security.core.userdetails.UserDetails;
|
||||
import org.springframework.security.core.userdetails.UsernameNotFoundException;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.util.UUID;
|
||||
|
||||
/**
|
||||
* REST controller for notification operations.
|
||||
*/
|
||||
@RestController
|
||||
@RequestMapping("/api/notifications")
|
||||
@RequiredArgsConstructor
|
||||
@Slf4j
|
||||
public class NotificationController {
|
||||
|
||||
private final NotificationService notificationService;
|
||||
private final UserRepository userRepository;
|
||||
|
||||
/**
|
||||
* Helper method to get user from authenticated UserDetails.
|
||||
*/
|
||||
private User getUser(UserDetails userDetails) {
|
||||
return userRepository.findByUsername(userDetails.getUsername())
|
||||
.orElseThrow(() -> new UsernameNotFoundException("User not found"));
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all notifications for the authenticated user.
|
||||
*
|
||||
* @param userDetails the authenticated user
|
||||
* @param pageable pagination parameters
|
||||
* @return page of notifications
|
||||
*/
|
||||
@GetMapping
|
||||
public ResponseEntity<Page<NotificationDTO>> getNotifications(
|
||||
@AuthenticationPrincipal UserDetails userDetails,
|
||||
Pageable pageable
|
||||
) {
|
||||
User user = getUser(userDetails);
|
||||
Page<Notification> notifications = notificationService.getNotifications(user.getId(), pageable);
|
||||
Page<NotificationDTO> notificationDTOs = notifications.map(NotificationDTO::fromEntity);
|
||||
return ResponseEntity.ok(notificationDTOs);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get unread notifications for the authenticated user.
|
||||
*
|
||||
* @param userDetails the authenticated user
|
||||
* @param pageable pagination parameters
|
||||
* @return page of unread notifications
|
||||
*/
|
||||
@GetMapping("/unread")
|
||||
public ResponseEntity<Page<NotificationDTO>> getUnreadNotifications(
|
||||
@AuthenticationPrincipal UserDetails userDetails,
|
||||
Pageable pageable
|
||||
) {
|
||||
User user = getUser(userDetails);
|
||||
Page<Notification> notifications = notificationService.getUnreadNotifications(user.getId(), pageable);
|
||||
Page<NotificationDTO> notificationDTOs = notifications.map(NotificationDTO::fromEntity);
|
||||
return ResponseEntity.ok(notificationDTOs);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get count of unread notifications.
|
||||
*
|
||||
* @param userDetails the authenticated user
|
||||
* @return count of unread notifications
|
||||
*/
|
||||
@GetMapping("/unread/count")
|
||||
public ResponseEntity<Map<String, Long>> getUnreadCount(
|
||||
@AuthenticationPrincipal UserDetails userDetails
|
||||
) {
|
||||
User user = getUser(userDetails);
|
||||
long count = notificationService.countUnreadNotifications(user.getId());
|
||||
Map<String, Long> response = new HashMap<>();
|
||||
response.put("count", count);
|
||||
return ResponseEntity.ok(response);
|
||||
}
|
||||
|
||||
/**
|
||||
* Mark a notification as read.
|
||||
*
|
||||
* @param notificationId the notification ID
|
||||
* @param userDetails the authenticated user
|
||||
* @return no content
|
||||
*/
|
||||
@PutMapping("/{notificationId}/read")
|
||||
@Transactional
|
||||
public ResponseEntity<Void> markAsRead(
|
||||
@PathVariable UUID notificationId,
|
||||
@AuthenticationPrincipal UserDetails userDetails
|
||||
) {
|
||||
User user = getUser(userDetails);
|
||||
boolean success = notificationService.markAsRead(notificationId, user.getId());
|
||||
|
||||
if (!success) {
|
||||
return ResponseEntity.notFound().build();
|
||||
}
|
||||
|
||||
return ResponseEntity.noContent().build();
|
||||
}
|
||||
|
||||
/**
|
||||
* Mark all notifications as read.
|
||||
*
|
||||
* @param userDetails the authenticated user
|
||||
* @return number of notifications marked as read
|
||||
*/
|
||||
@PutMapping("/read-all")
|
||||
@Transactional
|
||||
public ResponseEntity<Map<String, Integer>> markAllAsRead(
|
||||
@AuthenticationPrincipal UserDetails userDetails
|
||||
) {
|
||||
User user = getUser(userDetails);
|
||||
int count = notificationService.markAllAsRead(user.getId());
|
||||
Map<String, Integer> response = new HashMap<>();
|
||||
response.put("count", count);
|
||||
return ResponseEntity.ok(response);
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete a notification.
|
||||
*
|
||||
* @param notificationId the notification ID
|
||||
* @param userDetails the authenticated user
|
||||
* @return no content
|
||||
*/
|
||||
@DeleteMapping("/{notificationId}")
|
||||
@Transactional
|
||||
public ResponseEntity<Void> deleteNotification(
|
||||
@PathVariable UUID notificationId,
|
||||
@AuthenticationPrincipal UserDetails userDetails
|
||||
) {
|
||||
User user = getUser(userDetails);
|
||||
boolean success = notificationService.deleteNotification(notificationId, user.getId());
|
||||
|
||||
if (!success) {
|
||||
return ResponseEntity.notFound().build();
|
||||
}
|
||||
|
||||
return ResponseEntity.noContent().build();
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,58 @@
|
|||
package org.operaton.fitpub.model.dto;
|
||||
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Builder;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
import org.operaton.fitpub.model.entity.Notification;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.UUID;
|
||||
|
||||
/**
|
||||
* DTO for Notification data transfer.
|
||||
*/
|
||||
@Data
|
||||
@Builder
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public class NotificationDTO {
|
||||
|
||||
private UUID id;
|
||||
private String type;
|
||||
private String actorUri;
|
||||
private String actorDisplayName;
|
||||
private String actorUsername;
|
||||
private String actorAvatarUrl;
|
||||
private UUID activityId;
|
||||
private String activityTitle;
|
||||
private UUID commentId;
|
||||
private String commentText;
|
||||
private boolean read;
|
||||
private LocalDateTime createdAt;
|
||||
private LocalDateTime readAt;
|
||||
|
||||
/**
|
||||
* Creates a DTO from a Notification entity.
|
||||
*
|
||||
* @param notification the notification entity
|
||||
* @return notification DTO
|
||||
*/
|
||||
public static NotificationDTO fromEntity(Notification notification) {
|
||||
return NotificationDTO.builder()
|
||||
.id(notification.getId())
|
||||
.type(notification.getType().name())
|
||||
.actorUri(notification.getActorUri())
|
||||
.actorDisplayName(notification.getActorDisplayName())
|
||||
.actorUsername(notification.getActorUsername())
|
||||
.actorAvatarUrl(notification.getActorAvatarUrl())
|
||||
.activityId(notification.getActivityId())
|
||||
.activityTitle(notification.getActivityTitle())
|
||||
.commentId(notification.getCommentId())
|
||||
.commentText(notification.getCommentText())
|
||||
.read(notification.isRead())
|
||||
.createdAt(notification.getCreatedAt())
|
||||
.readAt(notification.getReadAt())
|
||||
.build();
|
||||
}
|
||||
}
|
||||
158
src/main/java/org/operaton/fitpub/model/entity/Notification.java
Normal file
158
src/main/java/org/operaton/fitpub/model/entity/Notification.java
Normal file
|
|
@ -0,0 +1,158 @@
|
|||
package org.operaton.fitpub.model.entity;
|
||||
|
||||
import jakarta.persistence.*;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Builder;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
import org.hibernate.annotations.CreationTimestamp;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.UUID;
|
||||
|
||||
/**
|
||||
* Entity representing a user notification.
|
||||
* Notifications are created for social interactions like likes, comments, follows, etc.
|
||||
*/
|
||||
@Entity
|
||||
@Table(name = "notifications", indexes = {
|
||||
@Index(name = "idx_notifications_user_id", columnList = "user_id"),
|
||||
@Index(name = "idx_notifications_read_status", columnList = "user_id, is_read"),
|
||||
@Index(name = "idx_notifications_created_at", columnList = "created_at")
|
||||
})
|
||||
@Data
|
||||
@Builder
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public class Notification {
|
||||
|
||||
@Id
|
||||
@GeneratedValue(strategy = GenerationType.AUTO)
|
||||
private UUID id;
|
||||
|
||||
/**
|
||||
* The user who receives this notification.
|
||||
*/
|
||||
@ManyToOne(fetch = FetchType.LAZY)
|
||||
@JoinColumn(name = "user_id", nullable = false)
|
||||
private User user;
|
||||
|
||||
/**
|
||||
* Type of notification.
|
||||
*/
|
||||
@Enumerated(EnumType.STRING)
|
||||
@Column(name = "type", nullable = false, length = 50)
|
||||
private NotificationType type;
|
||||
|
||||
/**
|
||||
* The user who triggered this notification (actor).
|
||||
* Can be null for system notifications.
|
||||
*/
|
||||
@Column(name = "actor_uri")
|
||||
private String actorUri;
|
||||
|
||||
/**
|
||||
* Display name of the actor (cached for performance).
|
||||
*/
|
||||
@Column(name = "actor_display_name")
|
||||
private String actorDisplayName;
|
||||
|
||||
/**
|
||||
* Username of the actor (cached for performance).
|
||||
*/
|
||||
@Column(name = "actor_username")
|
||||
private String actorUsername;
|
||||
|
||||
/**
|
||||
* Avatar URL of the actor (cached for performance).
|
||||
*/
|
||||
@Column(name = "actor_avatar_url")
|
||||
private String actorAvatarUrl;
|
||||
|
||||
/**
|
||||
* Related activity ID (for likes, comments on activities).
|
||||
*/
|
||||
@Column(name = "activity_id")
|
||||
private UUID activityId;
|
||||
|
||||
/**
|
||||
* Activity title (cached for performance).
|
||||
*/
|
||||
@Column(name = "activity_title")
|
||||
private String activityTitle;
|
||||
|
||||
/**
|
||||
* Related comment ID (if notification is about a comment).
|
||||
*/
|
||||
@Column(name = "comment_id")
|
||||
private UUID commentId;
|
||||
|
||||
/**
|
||||
* Comment text preview (cached for performance).
|
||||
*/
|
||||
@Column(name = "comment_text", length = 200)
|
||||
private String commentText;
|
||||
|
||||
/**
|
||||
* Whether the notification has been read.
|
||||
*/
|
||||
@Column(name = "is_read", nullable = false)
|
||||
@Builder.Default
|
||||
private boolean read = false;
|
||||
|
||||
/**
|
||||
* Timestamp when notification was created.
|
||||
*/
|
||||
@CreationTimestamp
|
||||
@Column(name = "created_at", nullable = false, updatable = false)
|
||||
private LocalDateTime createdAt;
|
||||
|
||||
/**
|
||||
* Timestamp when notification was read.
|
||||
*/
|
||||
@Column(name = "read_at")
|
||||
private LocalDateTime readAt;
|
||||
|
||||
/**
|
||||
* Types of notifications that can be sent.
|
||||
*/
|
||||
public enum NotificationType {
|
||||
/**
|
||||
* Someone liked your activity.
|
||||
*/
|
||||
ACTIVITY_LIKED,
|
||||
|
||||
/**
|
||||
* Someone commented on your activity.
|
||||
*/
|
||||
ACTIVITY_COMMENTED,
|
||||
|
||||
/**
|
||||
* Someone followed you.
|
||||
*/
|
||||
USER_FOLLOWED,
|
||||
|
||||
/**
|
||||
* Someone accepted your follow request.
|
||||
*/
|
||||
FOLLOW_ACCEPTED,
|
||||
|
||||
/**
|
||||
* Someone shared/announced your activity.
|
||||
*/
|
||||
ACTIVITY_SHARED,
|
||||
|
||||
/**
|
||||
* Someone mentioned you in a comment.
|
||||
*/
|
||||
MENTIONED_IN_COMMENT
|
||||
}
|
||||
|
||||
/**
|
||||
* Mark this notification as read.
|
||||
*/
|
||||
public void markAsRead() {
|
||||
this.read = true;
|
||||
this.readAt = LocalDateTime.now();
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,69 @@
|
|||
package org.operaton.fitpub.repository;
|
||||
|
||||
import org.operaton.fitpub.model.entity.Notification;
|
||||
import org.springframework.data.domain.Page;
|
||||
import org.springframework.data.domain.Pageable;
|
||||
import org.springframework.data.jpa.repository.JpaRepository;
|
||||
import org.springframework.data.jpa.repository.Modifying;
|
||||
import org.springframework.data.jpa.repository.Query;
|
||||
import org.springframework.data.repository.query.Param;
|
||||
import org.springframework.stereotype.Repository;
|
||||
|
||||
import java.util.UUID;
|
||||
|
||||
/**
|
||||
* Repository for Notification entity.
|
||||
*/
|
||||
@Repository
|
||||
public interface NotificationRepository extends JpaRepository<Notification, UUID> {
|
||||
|
||||
/**
|
||||
* Find all notifications for a user, ordered by creation date (newest first).
|
||||
*
|
||||
* @param userId the user ID
|
||||
* @param pageable pagination parameters
|
||||
* @return page of notifications
|
||||
*/
|
||||
@Query("SELECT n FROM Notification n WHERE n.user.id = :userId ORDER BY n.createdAt DESC")
|
||||
Page<Notification> findByUserId(@Param("userId") UUID userId, Pageable pageable);
|
||||
|
||||
/**
|
||||
* Find unread notifications for a user, ordered by creation date (newest first).
|
||||
*
|
||||
* @param userId the user ID
|
||||
* @param pageable pagination parameters
|
||||
* @return page of unread notifications
|
||||
*/
|
||||
@Query("SELECT n FROM Notification n WHERE n.user.id = :userId AND n.read = false ORDER BY n.createdAt DESC")
|
||||
Page<Notification> findUnreadByUserId(@Param("userId") UUID userId, Pageable pageable);
|
||||
|
||||
/**
|
||||
* Count unread notifications for a user.
|
||||
*
|
||||
* @param userId the user ID
|
||||
* @return count of unread notifications
|
||||
*/
|
||||
@Query("SELECT COUNT(n) FROM Notification n WHERE n.user.id = :userId AND n.read = false")
|
||||
long countUnreadByUserId(@Param("userId") UUID userId);
|
||||
|
||||
/**
|
||||
* Mark all notifications as read for a user.
|
||||
*
|
||||
* @param userId the user ID
|
||||
* @return number of notifications marked as read
|
||||
*/
|
||||
@Modifying
|
||||
@Query("UPDATE Notification n SET n.read = true, n.readAt = CURRENT_TIMESTAMP WHERE n.user.id = :userId AND n.read = false")
|
||||
int markAllAsReadByUserId(@Param("userId") UUID userId);
|
||||
|
||||
/**
|
||||
* Delete old read notifications for a user (older than a specified date).
|
||||
*
|
||||
* @param userId the user ID
|
||||
* @param cutoffDate the cutoff date
|
||||
* @return number of notifications deleted
|
||||
*/
|
||||
@Modifying
|
||||
@Query("DELETE FROM Notification n WHERE n.user.id = :userId AND n.read = true AND n.createdAt < :cutoffDate")
|
||||
int deleteOldReadNotifications(@Param("userId") UUID userId, @Param("cutoffDate") java.time.LocalDateTime cutoffDate);
|
||||
}
|
||||
|
|
@ -0,0 +1,282 @@
|
|||
package org.operaton.fitpub.service;
|
||||
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.operaton.fitpub.model.entity.*;
|
||||
import org.operaton.fitpub.repository.NotificationRepository;
|
||||
import org.operaton.fitpub.repository.RemoteActorRepository;
|
||||
import org.operaton.fitpub.repository.UserRepository;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.data.domain.Page;
|
||||
import org.springframework.data.domain.Pageable;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.UUID;
|
||||
|
||||
/**
|
||||
* Service for managing user notifications.
|
||||
*/
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
@Slf4j
|
||||
public class NotificationService {
|
||||
|
||||
private final NotificationRepository notificationRepository;
|
||||
private final UserRepository userRepository;
|
||||
private final RemoteActorRepository remoteActorRepository;
|
||||
|
||||
@Value("${fitpub.base-url}")
|
||||
private String baseUrl;
|
||||
|
||||
/**
|
||||
* Create a notification when someone likes an activity.
|
||||
*
|
||||
* @param activity the activity that was liked
|
||||
* @param likerActorUri the URI of the user who liked the activity
|
||||
*/
|
||||
@Transactional
|
||||
public void createActivityLikedNotification(Activity activity, String likerActorUri) {
|
||||
// Don't notify if user liked their own activity
|
||||
String activityOwnerUri = activity.getUser().getActorUri(baseUrl);
|
||||
if (activityOwnerUri.equals(likerActorUri)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Get actor information
|
||||
ActorInfo actorInfo = getActorInfo(likerActorUri);
|
||||
if (actorInfo == null) {
|
||||
log.warn("Could not find actor info for URI: {}", likerActorUri);
|
||||
return;
|
||||
}
|
||||
|
||||
Notification notification = Notification.builder()
|
||||
.user(activity.getUser())
|
||||
.type(Notification.NotificationType.ACTIVITY_LIKED)
|
||||
.actorUri(likerActorUri)
|
||||
.actorDisplayName(actorInfo.displayName)
|
||||
.actorUsername(actorInfo.username)
|
||||
.actorAvatarUrl(actorInfo.avatarUrl)
|
||||
.activityId(activity.getId())
|
||||
.activityTitle(activity.getTitle() != null ? activity.getTitle() : "Untitled Activity")
|
||||
.build();
|
||||
|
||||
notificationRepository.save(notification);
|
||||
log.debug("Created ACTIVITY_LIKED notification for user {} from {}", activity.getUser().getUsername(), actorInfo.username);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a notification when someone comments on an activity.
|
||||
*
|
||||
* @param activity the activity that was commented on
|
||||
* @param comment the comment
|
||||
* @param commenterActorUri the URI of the user who commented
|
||||
*/
|
||||
@Transactional
|
||||
public void createActivityCommentedNotification(Activity activity, Comment comment, String commenterActorUri) {
|
||||
// Don't notify if user commented on their own activity
|
||||
String activityOwnerUri = activity.getUser().getActorUri(baseUrl);
|
||||
if (activityOwnerUri.equals(commenterActorUri)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Get actor information
|
||||
ActorInfo actorInfo = getActorInfo(commenterActorUri);
|
||||
if (actorInfo == null) {
|
||||
log.warn("Could not find actor info for URI: {}", commenterActorUri);
|
||||
return;
|
||||
}
|
||||
|
||||
// Truncate comment text for preview
|
||||
String commentPreview = comment.getContent();
|
||||
if (commentPreview != null && commentPreview.length() > 200) {
|
||||
commentPreview = commentPreview.substring(0, 197) + "...";
|
||||
}
|
||||
|
||||
Notification notification = Notification.builder()
|
||||
.user(activity.getUser())
|
||||
.type(Notification.NotificationType.ACTIVITY_COMMENTED)
|
||||
.actorUri(commenterActorUri)
|
||||
.actorDisplayName(actorInfo.displayName)
|
||||
.actorUsername(actorInfo.username)
|
||||
.actorAvatarUrl(actorInfo.avatarUrl)
|
||||
.activityId(activity.getId())
|
||||
.activityTitle(activity.getTitle() != null ? activity.getTitle() : "Untitled Activity")
|
||||
.commentId(comment.getId())
|
||||
.commentText(commentPreview)
|
||||
.build();
|
||||
|
||||
notificationRepository.save(notification);
|
||||
log.debug("Created ACTIVITY_COMMENTED notification for user {} from {}", activity.getUser().getUsername(), actorInfo.username);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a notification when someone follows a user.
|
||||
*
|
||||
* @param followedUser the user who was followed
|
||||
* @param followerActorUri the URI of the user who followed
|
||||
*/
|
||||
@Transactional
|
||||
public void createUserFollowedNotification(User followedUser, String followerActorUri) {
|
||||
// Get actor information
|
||||
ActorInfo actorInfo = getActorInfo(followerActorUri);
|
||||
if (actorInfo == null) {
|
||||
log.warn("Could not find actor info for URI: {}", followerActorUri);
|
||||
return;
|
||||
}
|
||||
|
||||
Notification notification = Notification.builder()
|
||||
.user(followedUser)
|
||||
.type(Notification.NotificationType.USER_FOLLOWED)
|
||||
.actorUri(followerActorUri)
|
||||
.actorDisplayName(actorInfo.displayName)
|
||||
.actorUsername(actorInfo.username)
|
||||
.actorAvatarUrl(actorInfo.avatarUrl)
|
||||
.build();
|
||||
|
||||
notificationRepository.save(notification);
|
||||
log.debug("Created USER_FOLLOWED notification for user {} from {}", followedUser.getUsername(), actorInfo.username);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all notifications for a user.
|
||||
*
|
||||
* @param userId the user ID
|
||||
* @param pageable pagination parameters
|
||||
* @return page of notifications
|
||||
*/
|
||||
@Transactional(readOnly = true)
|
||||
public Page<Notification> getNotifications(UUID userId, Pageable pageable) {
|
||||
return notificationRepository.findByUserId(userId, pageable);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get unread notifications for a user.
|
||||
*
|
||||
* @param userId the user ID
|
||||
* @param pageable pagination parameters
|
||||
* @return page of unread notifications
|
||||
*/
|
||||
@Transactional(readOnly = true)
|
||||
public Page<Notification> getUnreadNotifications(UUID userId, Pageable pageable) {
|
||||
return notificationRepository.findUnreadByUserId(userId, pageable);
|
||||
}
|
||||
|
||||
/**
|
||||
* Count unread notifications for a user.
|
||||
*
|
||||
* @param userId the user ID
|
||||
* @return count of unread notifications
|
||||
*/
|
||||
@Transactional(readOnly = true)
|
||||
public long countUnreadNotifications(UUID userId) {
|
||||
return notificationRepository.countUnreadByUserId(userId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Mark a notification as read.
|
||||
*
|
||||
* @param notificationId the notification ID
|
||||
* @param userId the user ID (for authorization)
|
||||
* @return true if marked as read, false if not found or not owned by user
|
||||
*/
|
||||
@Transactional
|
||||
public boolean markAsRead(UUID notificationId, UUID userId) {
|
||||
return notificationRepository.findById(notificationId)
|
||||
.filter(n -> n.getUser().getId().equals(userId))
|
||||
.map(notification -> {
|
||||
if (!notification.isRead()) {
|
||||
notification.markAsRead();
|
||||
notificationRepository.save(notification);
|
||||
}
|
||||
return true;
|
||||
})
|
||||
.orElse(false);
|
||||
}
|
||||
|
||||
/**
|
||||
* Mark all notifications as read for a user.
|
||||
*
|
||||
* @param userId the user ID
|
||||
* @return number of notifications marked as read
|
||||
*/
|
||||
@Transactional
|
||||
public int markAllAsRead(UUID userId) {
|
||||
return notificationRepository.markAllAsReadByUserId(userId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete a notification.
|
||||
*
|
||||
* @param notificationId the notification ID
|
||||
* @param userId the user ID (for authorization)
|
||||
* @return true if deleted, false if not found or not owned by user
|
||||
*/
|
||||
@Transactional
|
||||
public boolean deleteNotification(UUID notificationId, UUID userId) {
|
||||
return notificationRepository.findById(notificationId)
|
||||
.filter(n -> n.getUser().getId().equals(userId))
|
||||
.map(notification -> {
|
||||
notificationRepository.delete(notification);
|
||||
return true;
|
||||
})
|
||||
.orElse(false);
|
||||
}
|
||||
|
||||
/**
|
||||
* Clean up old read notifications (older than 30 days).
|
||||
*
|
||||
* @param userId the user ID
|
||||
* @return number of notifications deleted
|
||||
*/
|
||||
@Transactional
|
||||
public int cleanupOldNotifications(UUID userId) {
|
||||
LocalDateTime cutoffDate = LocalDateTime.now().minusDays(30);
|
||||
return notificationRepository.deleteOldReadNotifications(userId, cutoffDate);
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper method to get actor information from either local users or remote actors.
|
||||
*
|
||||
* @param actorUri the actor URI
|
||||
* @return actor information or null if not found
|
||||
*/
|
||||
private ActorInfo getActorInfo(String actorUri) {
|
||||
// Check if it's a local user
|
||||
if (actorUri.startsWith(baseUrl)) {
|
||||
String username = actorUri.substring(actorUri.lastIndexOf("/") + 1);
|
||||
return userRepository.findByUsername(username)
|
||||
.map(user -> new ActorInfo(
|
||||
user.getDisplayName() != null ? user.getDisplayName() : user.getUsername(),
|
||||
user.getUsername(),
|
||||
user.getAvatarUrl()
|
||||
))
|
||||
.orElse(null);
|
||||
}
|
||||
|
||||
// Check if it's a remote actor
|
||||
return remoteActorRepository.findByActorUri(actorUri)
|
||||
.map(actor -> new ActorInfo(
|
||||
actor.getDisplayName() != null ? actor.getDisplayName() : actor.getUsername(),
|
||||
actor.getUsername(),
|
||||
actor.getAvatarUrl()
|
||||
))
|
||||
.orElse(null);
|
||||
}
|
||||
|
||||
/**
|
||||
* Internal class to hold actor information.
|
||||
*/
|
||||
private static class ActorInfo {
|
||||
String displayName;
|
||||
String username;
|
||||
String avatarUrl;
|
||||
|
||||
ActorInfo(String displayName, String username, String avatarUrl) {
|
||||
this.displayName = displayName;
|
||||
this.username = username;
|
||||
this.avatarUrl = avatarUrl;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,39 @@
|
|||
-- Create notifications table
|
||||
CREATE TABLE IF NOT EXISTS notifications (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
user_id UUID NOT NULL,
|
||||
type VARCHAR(50) NOT NULL,
|
||||
actor_uri VARCHAR(500),
|
||||
actor_display_name VARCHAR(255),
|
||||
actor_username VARCHAR(255),
|
||||
actor_avatar_url TEXT,
|
||||
activity_id UUID,
|
||||
activity_title VARCHAR(255),
|
||||
comment_id UUID,
|
||||
comment_text VARCHAR(200),
|
||||
is_read BOOLEAN NOT NULL DEFAULT FALSE,
|
||||
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
read_at TIMESTAMP,
|
||||
CONSTRAINT fk_notifications_user FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE,
|
||||
CONSTRAINT fk_notifications_activity FOREIGN KEY (activity_id) REFERENCES activities(id) ON DELETE CASCADE,
|
||||
CONSTRAINT fk_notifications_comment FOREIGN KEY (comment_id) REFERENCES comments(id) ON DELETE CASCADE
|
||||
);
|
||||
|
||||
-- Create indexes for efficient queries
|
||||
CREATE INDEX idx_notifications_user_id ON notifications(user_id);
|
||||
CREATE INDEX idx_notifications_read_status ON notifications(user_id, is_read);
|
||||
CREATE INDEX idx_notifications_created_at ON notifications(created_at);
|
||||
|
||||
-- Comments
|
||||
COMMENT ON TABLE notifications IS 'User notifications for social interactions';
|
||||
COMMENT ON COLUMN notifications.type IS 'Type of notification: ACTIVITY_LIKED, ACTIVITY_COMMENTED, USER_FOLLOWED, FOLLOW_ACCEPTED, ACTIVITY_SHARED, MENTIONED_IN_COMMENT';
|
||||
COMMENT ON COLUMN notifications.actor_uri IS 'URI of the user who triggered the notification';
|
||||
COMMENT ON COLUMN notifications.actor_display_name IS 'Cached display name of the actor';
|
||||
COMMENT ON COLUMN notifications.actor_username IS 'Cached username of the actor';
|
||||
COMMENT ON COLUMN notifications.actor_avatar_url IS 'Cached avatar URL of the actor';
|
||||
COMMENT ON COLUMN notifications.activity_id IS 'Related activity ID (for likes, comments)';
|
||||
COMMENT ON COLUMN notifications.activity_title IS 'Cached activity title';
|
||||
COMMENT ON COLUMN notifications.comment_id IS 'Related comment ID';
|
||||
COMMENT ON COLUMN notifications.comment_text IS 'Preview of comment text';
|
||||
COMMENT ON COLUMN notifications.is_read IS 'Whether the notification has been read';
|
||||
COMMENT ON COLUMN notifications.read_at IS 'Timestamp when notification was read';
|
||||
Loading…
Add table
Add a link
Reference in a new issue