Files
xxx-sphere-web/src/main/java/de/oaa/xxx/social/SocialController.java
Mario d386f5a7a9
Some checks failed
Host-Based Deploy (Java 21 Fix) / build-and-run (push) Has been cancelled
Weiter am Dating gearbeitet
2026-04-04 00:09:08 +02:00

442 lines
20 KiB
Java

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<UserProfile> 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<List<UserProfile>> searchUsers(@RequestParam String q, Principal principal) {
UUID myId = userService.requireUser(principal).getUserId();
List<UserEntity> results = userRepository.findByNameContainingIgnoreCase(q);
List<UserProfile> 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<Void> 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<Void> 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<Void> 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<List<UserProfile>> getFriendsOfUser(@PathVariable("userId") UUID userId, Principal principal) {
userService.requireUser(principal);
List<UserProfile> 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<List<FriendshipDto>> getFriends(Principal principal) {
UUID myId = userService.requireUser(principal).getUserId();
List<FriendshipDto> 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<List<FriendshipDto>> getPendingRequests(Principal principal) {
UUID myId = userService.requireUser(principal).getUserId();
List<FriendshipDto> 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<Long> getPendingCount(Principal principal) {
UUID myId = userService.requireUser(principal).getUserId();
return ResponseEntity.ok(friendshipRepository.countByReceiverIdAndStatus(myId, Status.PENDING));
}
// ── Messages ──
@PostMapping("/messages")
public ResponseEntity<Map<String, String>> 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<List<ConversationSummary>> getConversations(Principal principal) {
UUID myId = userService.requireUser(principal).getUserId();
List<MessageEntity> allMessages = messageRepository.findAllByUser(myId);
// Group by partner, keep most recent message per partner
Map<UUID, MessageEntity> latestByPartner = new LinkedHashMap<>();
for (MessageEntity m : allMessages) {
UUID partnerId = m.getSenderId().equals(myId) ? m.getReceiverId() : m.getSenderId();
latestByPartner.putIfAbsent(partnerId, m);
}
List<ConversationSummary> summaries = new ArrayList<>();
for (Map.Entry<UUID, MessageEntity> 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<Long> 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<MessageEntity> 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<MessageEntity> 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<Void> 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<Void> 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<Map<String, Boolean>> 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);
}
}