package de.oaa.xxx.social; import de.oaa.xxx.dating.DatingMatchRepository; import de.oaa.xxx.social.dto.ConversationSummary; import de.oaa.xxx.social.dto.FriendshipDto; import de.oaa.xxx.social.dto.MessageDto; import de.oaa.xxx.social.dto.UserProfile; import de.oaa.xxx.social.entity.BlockEntity; import de.oaa.xxx.social.entity.FriendshipEntity; import de.oaa.xxx.social.entity.FriendshipEntity.Status; import de.oaa.xxx.social.entity.MessageCause; import de.oaa.xxx.social.entity.MessageEntity; import de.oaa.xxx.social.repository.BlockRepository; import de.oaa.xxx.social.repository.FriendshipRepository; import de.oaa.xxx.social.repository.MessageRepository; import de.oaa.xxx.subscription.SubscriptionLimitService; import de.oaa.xxx.support.SupportUserService; import de.oaa.xxx.user.UserEntity; import de.oaa.xxx.user.UserRepository; import de.oaa.xxx.user.UserService; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.data.domain.PageRequest; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.*; import java.security.Principal; import java.time.LocalDateTime; import java.util.*; @RestController @RequestMapping("/social") public class SocialController { private static final Logger LOGGER = LoggerFactory.getLogger(SocialController.class); private final UserRepository userRepository; private final FriendshipRepository friendshipRepository; private final MessageRepository messageRepository; private final BlockRepository blockRepository; private final DatingMatchRepository datingMatchRepository; private final SubscriptionLimitService subscriptionLimitService; private final SseService sseService; private final SystemMessageService systemMessageService; private final UserService userService; public SocialController(UserRepository userRepository, FriendshipRepository friendshipRepository, MessageRepository messageRepository, BlockRepository blockRepository, DatingMatchRepository datingMatchRepository, SubscriptionLimitService subscriptionLimitService, SseService sseService, SystemMessageService systemMessageService, UserService userService) { this.userRepository = userRepository; this.friendshipRepository = friendshipRepository; this.messageRepository = messageRepository; this.blockRepository = blockRepository; this.datingMatchRepository = datingMatchRepository; this.subscriptionLimitService = subscriptionLimitService; this.sseService = sseService; this.systemMessageService = systemMessageService; this.userService = userService; } record FriendRequestBody(UUID receiverId) {} record FriendshipActionBody(UUID friendshipId) {} record SendMessageBody(UUID receiverId, String text) {} // ── User Profile ── @GetMapping("/users/{userId}") public ResponseEntity getUserProfile(@PathVariable("userId") UUID userId, Principal principal) { UUID myId = userService.requireUser(principal).getUserId(); return userRepository.findById(userId) .map(u -> ResponseEntity.ok(toUserProfileWithStatus(u, myId))) .orElse(ResponseEntity.notFound().build()); } // ── User Search ── @GetMapping("/users/search") public ResponseEntity> searchUsers(@RequestParam String q, Principal principal) { UUID myId = userService.requireUser(principal).getUserId(); List results = userRepository.findByNameContainingIgnoreCase(q); List profiles = results.stream() .filter(u -> !u.getUserId().equals(myId)) .limit(20) .map(u -> toUserProfileWithStatus(u, myId)) .toList(); return ResponseEntity.ok(profiles); } // ── Friendship ── @PostMapping("/friends/request") public ResponseEntity sendFriendRequest(@RequestBody FriendRequestBody body, Principal principal) { var me = userService.requireUser(principal); UUID myId = me.getUserId(); if (myId.equals(body.receiverId())) { return ResponseEntity.badRequest().build(); } if (friendshipRepository.findExisting(myId, body.receiverId()).isPresent()) { return ResponseEntity.status(409).build(); } FriendshipEntity f = new FriendshipEntity(); f.setFriendshipId(UUID.randomUUID()); f.setSenderId(myId); f.setReceiverId(body.receiverId()); f.setStatus(Status.PENDING); f.setCreatedAt(LocalDateTime.now()); friendshipRepository.save(f); LOGGER.info("User {} hat Freundschaftsanfrage an User {} gesendet", myId, body.receiverId()); String senderName = me.getName(); systemMessageService.send(myId, body.receiverId(), senderName + " hat dir eine Freundschaftsanfrage gesendet.", "/community/benutzer.html?userId=" + myId, MessageCause.FRIENDREQUEST); return ResponseEntity.status(201).build(); } @PostMapping("/friends/accept") public ResponseEntity acceptFriendRequest(@RequestBody FriendshipActionBody body, Principal principal) { UUID myId = userService.requireUser(principal).getUserId(); var fOpt = friendshipRepository.findById(body.friendshipId()); if (fOpt.isEmpty()) return ResponseEntity.notFound().build(); FriendshipEntity f = fOpt.get(); if (!f.getReceiverId().equals(myId)) return ResponseEntity.status(403).build(); f.setStatus(Status.ACCEPTED); friendshipRepository.save(f); LOGGER.info("User {} hat Freundschaftsanfrage {} angenommen", myId, body.friendshipId()); return ResponseEntity.ok().build(); } @DeleteMapping("/friends/reject") public ResponseEntity rejectOrRemoveFriend(@RequestBody FriendshipActionBody body, Principal principal) { UUID myId = userService.requireUser(principal).getUserId(); var fOpt = friendshipRepository.findById(body.friendshipId()); if (fOpt.isEmpty()) return ResponseEntity.notFound().build(); FriendshipEntity f = fOpt.get(); if (!f.getSenderId().equals(myId) && !f.getReceiverId().equals(myId)) { return ResponseEntity.status(403).build(); } friendshipRepository.delete(f); LOGGER.info("User {} hat Freundschaft/Anfrage {} gelöscht", myId, body.friendshipId()); return ResponseEntity.noContent().build(); } @GetMapping("/friends/user/{userId}") public ResponseEntity> getFriendsOfUser(@PathVariable("userId") UUID userId, Principal principal) { userService.requireUser(principal); List profiles = friendshipRepository.findFriends(userId, Status.ACCEPTED).stream() .map(f -> { UUID friendId = f.getSenderId().equals(userId) ? f.getReceiverId() : f.getSenderId(); return userRepository.findById(friendId) .map(u -> new UserProfile(u.getUserId(), u.getName(), u.getProfilePicture(), u.getProfilePictureHq(), null)) .orElse(null); }) .filter(Objects::nonNull) .toList(); return ResponseEntity.ok(profiles); } @GetMapping("/friends") public ResponseEntity> getFriends(Principal principal) { UUID myId = userService.requireUser(principal).getUserId(); List dtos = friendshipRepository.findFriends(myId, Status.ACCEPTED).stream() .map(f -> { UUID friendId = f.getSenderId().equals(myId) ? f.getReceiverId() : f.getSenderId(); return userRepository.findById(friendId) .map(u -> new FriendshipDto( f.getFriendshipId(), new UserProfile(u.getUserId(), u.getName(), u.getProfilePicture(), u.getProfilePictureHq(), "FRIEND"), f.getStatus().name(), f.getCreatedAt())) .orElse(null); }) .filter(Objects::nonNull) .toList(); return ResponseEntity.ok(dtos); } @GetMapping("/friends/pending") public ResponseEntity> getPendingRequests(Principal principal) { UUID myId = userService.requireUser(principal).getUserId(); List dtos = friendshipRepository.findByReceiverIdAndStatus(myId, Status.PENDING).stream() .map(f -> userRepository.findById(f.getSenderId()) .map(u -> new FriendshipDto( f.getFriendshipId(), new UserProfile(u.getUserId(), u.getName(), u.getProfilePicture(), u.getProfilePictureHq(), "PENDING_RECEIVED"), f.getStatus().name(), f.getCreatedAt())) .orElse(null)) .filter(Objects::nonNull) .toList(); return ResponseEntity.ok(dtos); } @GetMapping("/friends/pending/count") public ResponseEntity getPendingCount(Principal principal) { UUID myId = userService.requireUser(principal).getUserId(); return ResponseEntity.ok(friendshipRepository.countByReceiverIdAndStatus(myId, Status.PENDING)); } // ── Messages ── @PostMapping("/messages") public ResponseEntity> sendMessage(@RequestBody SendMessageBody body, Principal principal) { UUID myId = userService.requireUser(principal).getUserId(); if (body.text() == null || body.text().isBlank()) return ResponseEntity.badRequest().build(); // Nachrichten an den Support-Account sind nicht erlaubt if (SupportUserService.SUPPORT_USER_ID.equals(body.receiverId())) { return ResponseEntity.status(403).build(); } // Blockiert? (in beide Richtungen) if (blockRepository.existsBlock(myId, body.receiverId())) { return ResponseEntity.status(403).body(Map.of("reason", "BLOCKED")); } // Erste Nachricht in dieser Konversation → Bedingungen prüfen if (!messageRepository.conversationExists(myId, body.receiverId())) { boolean areFriends = friendshipRepository.findExisting(myId, body.receiverId()) .filter(f -> f.getStatus() == Status.ACCEPTED).isPresent(); boolean haveMatch = datingMatchRepository.existsByUsers(myId, body.receiverId()); boolean hasPro = subscriptionLimitService.hasActivePaidSubscription(myId); if (!areFriends && !haveMatch && !hasPro) { return ResponseEntity.status(403).body(Map.of("reason", "FIRST_MESSAGE_RESTRICTED")); } } MessageEntity msg = new MessageEntity(); msg.setMessageId(UUID.randomUUID()); msg.setSenderId(myId); msg.setReceiverId(body.receiverId()); msg.setText(body.text().trim()); msg.setSentAt(LocalDateTime.now()); messageRepository.save(msg); LOGGER.debug("User {} hat Nachricht an User {} gesendet", myId, body.receiverId()); long unread = messageRepository.countUnread(body.receiverId()); sseService.push(body.receiverId(), "DM", Map.of("unreadCount", unread, "senderId", myId.toString())); return ResponseEntity.status(201).build(); } @GetMapping("/messages") public ResponseEntity> getConversations(Principal principal) { UUID myId = userService.requireUser(principal).getUserId(); List allMessages = messageRepository.findAllByUser(myId); // Group by partner, keep most recent message per partner Map latestByPartner = new LinkedHashMap<>(); for (MessageEntity m : allMessages) { UUID partnerId = m.getSenderId().equals(myId) ? m.getReceiverId() : m.getSenderId(); latestByPartner.putIfAbsent(partnerId, m); } List summaries = new ArrayList<>(); for (Map.Entry entry : latestByPartner.entrySet()) { UUID partnerId = entry.getKey(); MessageEntity lastMsg = entry.getValue(); var partnerOpt = userRepository.findById(partnerId); if (partnerOpt.isEmpty()) continue; UserProfile partnerProfile = toUserProfileWithStatus(partnerOpt.get(), myId); MessageDto lastMsgDto = toMessageDto(lastMsg); long unreadCount = allMessages.stream() .filter(m -> m.getSenderId().equals(partnerId) && m.getReceiverId().equals(myId) && m.getReadAt() == null) .count(); summaries.add(new ConversationSummary(partnerProfile, lastMsgDto, unreadCount)); } return ResponseEntity.ok(summaries); } @GetMapping("/messages/unread/count") public ResponseEntity getUnreadCount(Principal principal) { UUID myId = userService.requireUser(principal).getUserId(); return ResponseEntity.ok(messageRepository.countUnread(myId)); } private static final int MSG_PAGE_SIZE = 20; @GetMapping("/messages/{partnerId}") public ResponseEntity getConversation( @PathVariable("partnerId") UUID partnerId, @RequestParam(required = false) String before, @RequestParam(required = false) String after, Principal principal) { UUID myId = userService.requireUser(principal).getUserId(); if (after != null) { LocalDateTime afterDt = LocalDateTime.parse(after); List newMsgs = messageRepository.findConversationAfter(myId, partnerId, afterDt); messageRepository.markAsRead(myId, partnerId, LocalDateTime.now()); return ResponseEntity.ok(Map.of("messages", newMsgs.stream().map(this::toMessageDto).toList(), "hasMore", false)); } List messages; if (before != null) { LocalDateTime beforeDt = LocalDateTime.parse(before); messages = new ArrayList<>(messageRepository.findConversationBefore(myId, partnerId, beforeDt, PageRequest.of(0, MSG_PAGE_SIZE + 1))); } else { messages = new ArrayList<>(messageRepository.findConversation(myId, partnerId, PageRequest.of(0, MSG_PAGE_SIZE + 1))); messageRepository.markAsRead(myId, partnerId, LocalDateTime.now()); } boolean hasMore = messages.size() > MSG_PAGE_SIZE; if (hasMore) messages = messages.subList(0, MSG_PAGE_SIZE); // DESC order from DB → reverse to oldest-first for client Collections.reverse(messages); return ResponseEntity.ok(Map.of("messages", messages.stream().map(this::toMessageDto).toList(), "hasMore", hasMore)); } // ── Block ── @PostMapping("/block/{userId}") public ResponseEntity blockUser(@PathVariable("userId") UUID targetId, Principal principal) { UUID myId = userService.requireUser(principal).getUserId(); if (myId.equals(targetId)) return ResponseEntity.badRequest().build(); // Bereits blockiert? if (blockRepository.findByBlockerIdAndBlockedId(myId, targetId).isPresent()) { return ResponseEntity.status(409).build(); } // Block speichern BlockEntity block = new BlockEntity(); block.setBlockId(UUID.randomUUID()); block.setBlockerId(myId); block.setBlockedId(targetId); block.setBlockedAt(LocalDateTime.now()); blockRepository.save(block); LOGGER.info("User {} hat User {} blockiert", myId, targetId); // Gesamte Konversation löschen messageRepository.deleteConversation(myId, targetId); // Bestehende Freundschaft aufheben friendshipRepository.findExisting(myId, targetId).ifPresent(friendshipRepository::delete); return ResponseEntity.status(201).build(); } @DeleteMapping("/block/{userId}") public ResponseEntity unblockUser(@PathVariable("userId") UUID targetId, Principal principal) { UUID myId = userService.requireUser(principal).getUserId(); if (blockRepository.findByBlockerIdAndBlockedId(myId, targetId).isEmpty()) { return ResponseEntity.notFound().build(); } blockRepository.deleteByBlockerIdAndBlockedId(myId, targetId); LOGGER.info("User {} hat User {} entblockt", myId, targetId); return ResponseEntity.noContent().build(); } @GetMapping("/block/{userId}") public ResponseEntity> getBlockStatus(@PathVariable("userId") UUID targetId, Principal principal) { UUID myId = userService.requireUser(principal).getUserId(); boolean blockedByMe = blockRepository.findByBlockerIdAndBlockedId(myId, targetId).isPresent(); return ResponseEntity.ok(Map.of("blockedByMe", blockedByMe)); } // ── Helpers ── private UserProfile toUserProfileWithStatus(UserEntity user, UUID myId) { boolean isOwn = user.getUserId().equals(myId); String status = "NONE"; if (!isOwn) { var existing = friendshipRepository.findExisting(myId, user.getUserId()); if (existing.isPresent()) { FriendshipEntity f = existing.get(); if (f.getStatus() == Status.ACCEPTED) { status = "FRIEND"; } else if (f.getSenderId().equals(myId)) { status = "PENDING_SENT"; } else { status = "PENDING_RECEIVED"; } } } boolean isFriend = isOwn || "FRIEND".equals(status); // Grunddaten nur zurückgeben wenn berechtigt de.oaa.xxx.user.Sichtbarkeit svGd = user.getSichtbarkeitGrunddaten(); boolean showGrunddaten = isOwn || svGd == de.oaa.xxx.user.Sichtbarkeit.ALLE || (svGd == de.oaa.xxx.user.Sichtbarkeit.NUR_FREUNDE && isFriend); // XP nur zurückgeben wenn berechtigt de.oaa.xxx.user.Sichtbarkeit svXp = user.getSichtbarkeitXp(); boolean showXp = isOwn || svXp == de.oaa.xxx.user.Sichtbarkeit.ALLE || (svXp == de.oaa.xxx.user.Sichtbarkeit.NUR_FREUNDE && isFriend); return new UserProfile( user.getUserId(), user.getName(), user.getProfilePicture(), user.getProfilePictureHq(), status, showGrunddaten ? user.getAlter() : null, showGrunddaten ? user.getGroesse() : null, showGrunddaten ? user.getGewicht() : null, showGrunddaten ? user.getGeschlecht() : null, showGrunddaten ? user.getNeigung() : null, showGrunddaten ? user.getBeziehungsstatus() : null, showGrunddaten ? user.getBeschreibung() : null, showXp ? user.getLockeeXp() : 0, showXp ? user.getKeyholderXp() : 0, showXp ? user.getBdsmXp() : 0, user.getSichtbarkeitGrunddaten(), user.getSichtbarkeitGalerie(), user.getSichtbarkeitFreunde(), user.getSichtbarkeitFeed(), user.getSichtbarkeitPinnwand(), user.getSichtbarkeitXp(), user.getSichtbarkeitLockhistorie(), user.getSichtbarkeitVorlieben(), user.isProfilBeiVeroeffentlichungenSichtbar(), user.isDatingAktiv()); } private MessageDto toMessageDto(MessageEntity m) { String senderName = userRepository.findById(m.getSenderId()) .map(UserEntity::getName) .orElse("Unbekannt"); return new MessageDto( m.getMessageId(), m.getSenderId(), senderName, m.getReceiverId(), m.getText(), m.getSentAt(), m.getReadAt() != null); } }