Some checks failed
Host-Based Deploy (Java 21 Fix) / build-and-run (push) Has been cancelled
442 lines
20 KiB
Java
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);
|
|
}
|
|
}
|